Programování pro řízení ve Windows Richard Šusta
Částečně opravená verze – není plně korigovaná. Vydalo Vydavatelství ČVUT-FEL, Zikova 4, 166 35 Praha 6 v lednu 1999 jako svou 9292 publikaci. ISBN 80-01-01922-5
Elektronická podoba určená pouze ke studijním účelům.
2
Obsah 0 1
Programování pro řízení ve Windows.......................................................................................... 6 Opakování některých důležitých pojmů jazyka C a C++......................................................... 10 1.1 Něco málo o historii jazyka C...................................................................................10 1.2 Atributy datových objektů ........................................................................................11 1.2a Scope (rozsah platnosti) ........................................................................................11 1.2b Viditelnost identifikátorů a operátor :: ..................................................................13 1.2c Duration .................................................................................................................16 1.2d Linkage a program ve více souborech .....................................................................16 1.3 Pointry a adresace procesoru ....................................................................................26 1.3a Adresový prostor procesoru...................................................................................27 1.3b Procesor 8086 a MS-DOS......................................................................................28 1.3c Procesor 80286 a Win16 .......................................................................................30 1.3d Procesory 80386 až Pentium a Win32....................................................................31 1.3e Modely rozložení paměti .......................................................................................33 1.4 Pointry v jazyce C ....................................................................................................36 1.4a Typy pointrů .........................................................................................................36 1.4b Základní operace s pointry ....................................................................................37 1.4c Pointry na pointry a na funkce...............................................................................42 1.4d Operátory new a delete..........................................................................................43 1.5 Reference .................................................................................................................45 2 - Cesta k objektům ....................................................................................................................... 48 2.1 Požadavky na objekty ...............................................................................................48 2.2 Jednolitý program ....................................................................................................49 2.3 Podprogramy ............................................................................................................50 2.4 Struktury ..................................................................................................................52 2.5 Objektový program...................................................................................................54 2.5a Viditelnost proměnných a metod ...........................................................................55 2.5b Konstruktory a destruktory....................................................................................56 2.6 Atributy přístupu ......................................................................................................58 2.7 Jak si vytvořit vlastní objekt.....................................................................................60 3 - Více o objektech ......................................................................................................................... 63 3.1 Metody inline ...........................................................................................................63 3.2 Přetěžování (overloading) funkcí a metod v C++ ......................................................66 3.3 Dočasné objekty .......................................................................................................67 3.4 Dědění objektů .........................................................................................................70 3.5 Dědičnost a konstruktory a destruktory.....................................................................73 3.6 Atributy přístupu pro odvozené třídy ........................................................................73 3.7 Viditelnost proměnných při dědění ...........................................................................78 3.8 Deklarace friend.......................................................................................................83 3.9 Objekty a konstanty jako prvky jiných objektů .........................................................84 3.10 Data a metody typu static .........................................................................................85 3.11 Předefinování operátorů ...........................................................................................87 3.11a Operátor [].........................................................................................................88 3.11b Operátory ++ a -- ...............................................................................................88 3.11c Operátor = .........................................................................................................89 3.11d Operátory logického porovnání ..........................................................................90 3.11e Operátory >> a << .............................................................................................90 3.12 Virtuální metody ......................................................................................................91 3.13 Template ..................................................................................................................95 3.14 Objekty a implicitní znalosti ....................................................................................98
3
4
5
6
7
8
Architektura programů pro Windows ..................................................................................... 101 4.1 Více-... ...................................................................................................................101 4.2 Chronologie vzniku Windows.................................................................................102 4.3 Datové typy............................................................................................................105 4.4 Callback funkce .....................................................................................................106 4.5 Server a klienti .......................................................................................................107 4.5a Handle ................................................................................................................107 4.5b Okno ...................................................................................................................108 4.5c Zprávy ve Win32 ................................................................................................110 4.6 Architektura programu pro Win32 ..........................................................................113 4.6a Console aplikace .................................................................................................113 4.6b GUI aplikace ........................................................................................................113 4.7 Zdrojový text aplikace pro Windows ......................................................................116 4.8 Soubor aplikace......................................................................................................119 Kapitola 5 - Windows API......................................................................................................... 121 5.1 První aplikace pro Windows ...................................................................................122 5.2 Resource ................................................................................................................123 5.3 Program - WinAPI.C ..............................................................................................125 5.3a Vytvoření okna a hlavní smyčka zpráv ................................................................125 5.3b Callback funkce okna ...........................................................................................128 5.3c Příkazy................................................................................................................131 5.3d Kreslení okna a DC..............................................................................................132 5.4 Definice modulu.....................................................................................................135 - Objektový program pro Windows ......................................................................................... 136 6.1 Převedení WinAPI do programu v OWL .................................................................137 6.1a Okno jako klient okna .........................................................................................139 6.1b Event handlers - obsluha událostí ..........................................................................140 6.1c Výsledný zdrojový kód OWL aplikace ................................................................143 6.1d Náměty pro rozšíření aplikace .............................................................................147 6.2 Otevírání a zavírání oken v OWL ...........................................................................148 6.2a Vytvoření okna ...................................................................................................148 6.2b Rušení okna ........................................................................................................149 6.3 MFC ......................................................................................................................149 Prvky Control a komponenty.................................................................................................... 154 7.1 Dialogy a prvky control na úrovni Windows API ....................................................155 7.2 Zprávy prvků control ..............................................................................................161 7.3 Předdefinované dialogy ..........................................................................................163 7.3a Výstup textu .......................................................................................................163 7.3b Další předdefinované dialogy ..............................................................................165 7.4 Dialogy a prvky control v objektových knihovnách.................................................166 7.5 VCL .......................................................................................................................168 7.5a Properties.............................................................................................................168 7.5b Komponenty..........................................................................................................169 7.5c Vlastnosti a struktura VCL ......................................................................................170 7.5d Pøíklad VCL programu ...........................................................................................172 Zvláštnosti programů ve Win32................................................................................................ 175 8.1 Charakteristiky Win32............................................................................................175 8.2 Soubory s dlouhými jmény .....................................................................................177 8.3 Systémový registr ...................................................................................................180 8.4 Threads- vlákna......................................................................................................182 8.5 Zpracování výjimek................................................................................................187 8.5a C++ výjimky .......................................................................................................187
4
8.5b Strukturované výjimky ........................................................................................189 8.6 Virtuální alokace....................................................................................................191 8.7 Mapování souborů a sdílení paměti ........................................................................196 9 Služby OS a dynamické knihovny ............................................................................................ 200 9.1 Principy volání služeb systému...............................................................................200 9.1a Služby MS-DOSu................................................................................................200 9.1b Dynamické knihovny Win32 ...............................................................................202 9.2 Vytvoření dynamické knihovny ve Win32 ..............................................................205 9.2a Vstupní bod dynamické knihovny........................................................................205 9.2b Kód programu .....................................................................................................207 9.2c Soubor DEF - Module Definition File .......................................................................208 9.2d Zjednodušení kódu dynamické knihovny .............................................................209 9.2e Volání funkcí dynamické knihovny .....................................................................210 9.3 Odlišnosti dynamických knihoven ve Win16...........................................................212 10 Závěr ........................................................................................................................................ 214 10.1 Další zajímavá témata Win32 .................................................................................214 10.2 Literatura ...............................................................................................................216 10.3 Rejstřík pojmů .......................................................................................................218
5
0 Programování pro řízení ve Windows Předpoklady Skripta jsou určená pro přednášky k předmětu Programovací jazyky pro řízení (35PJR), který se vyučuje v letním semestru 3. ročníku na Katedře řídicí techniky FEL. Výklad předpokládá, že studenti: 1. ovládají syntaxi jazyka C, buď na základě absolvování předmětu Programovací jazyk C (36PJC) Katedry počítačů anebo díky vlastnímu samostudiu; 2. dovedou napsat program v jazyce C pro textově orientované prostředí, jako třeba pro MS-DOS (respektivě pro UNIX nebo Win32 Console); 3. mají povědomí o objektech. Přehled požadovaných znalostí jazyka C se uvádí na WEB stránce: http://novell.felk.cvut.cz/~susta.control/skripta/
Přehled obsahu Skripta shrnují vědomosti nutné k přechodu od C programů k jednoduchým objektovým aplikacím v jazyce C++ v prostředí 32-bitových Windows; čili Windows 95, 98 a NT, na něž se dále odkazuje pod společným názvem Win32. Výklad programování Win32 se omezuje na nezbytné základy. Uvádějí se četné příklady, ale jednotlivé programovací techniky se nerozvádějí do hloubky a to hned z několika důvodů jednak pro silně omezený rozsah skripta a jednak kvůli tomu, že v rozhraní Windows API, Application Interface, které tvoří jediný sjednocující element pro všechny programy, se dnes píší pouze dílčí operace. Většina zdrojového kódu se realizuje pomocí nadstaveb nad API, zpravidla založených na objektových knihovnách, a proto jakýkoliv hlubší výklad programování Windows se neobejde bez detailního popisu konkrétního vývojového prostředí. K těm existují manuály a podrobné on-line nápovědy, avšak jejich studium předpokládá znalosti základů jazyka C++ a principů činnosti Windows. A právě ty tvoří hlavní témata skripta. • Kapitola 1 opakuje obtížnější pojmy z jazyka C. Jejich výběr vychází z mých zkušeností s výukou. Probírají se atributy proměnných, stavba programu složeného z několika souborů, adresace procesorů a paměťové modely flat a large, zacházení s pointry, referencemi a dynamické alokace paměti. • Kapitola 2 uvádí do objektů. Vysvětluje jejich principy a možnosti na příkladu jednoduchého programu, který se postupně přetváří z klasického tvaru do objektové struktury C++. • Kapitola 3 rozšiřuje výklad kapitoly 2 a zabývá se nejčastějšími konstrukcemi v objektech. • Kapitola 4 se věnuje základům Windows, zejména zprávám a struktuře aplikace. • Kapitola 5 objasňuje tvorbu aplikací pro Windows API pomocí ukázky programu. • Kapitola 6 rozebírá principy objektových knihoven a jejich použití pro tvorbu aplikací pro Windows. Příklad z kapitoly 5 se v ní převede do objektové knihovny OWL, firmy Borland, a řešení se srovná s knihovnou MFC, firmy Microsoft • Kapitola 7 se zabývá dialogy a prvky typu Control (jako tlačítka, dialogy). V závěru se příklad z kapitoly 6 převede do knihovny VCL, používané v Borland C++ Builder . • Kapitola 8 se zaměřuje na přínosy prostředí Win32 - na dlouhá jména souborů, systémový registr, spouštění paralelních vláken, virtuální alokace, mapování souborů a sdílení paměti. • Kapitola 9 popisuje mechanismus volání služeb Windows a vysvětluje tvorbu dynamických knihoven. • Závěr - zahrnuje přehled neprobraných témat, seznam literatury opatřený krátkými recenzemi jednotlivých knih a rejstřík pojmů. V poznámkách se uvádějí i odchylky pro 16-ti bitová Windows 3.1, k nimž se dále referuje jako k Win16.
6
Uplatnění znalostí o Win32 v řízení Význam Win32 pro řízení vyplývá ze struktury automatizace výroby Ta zahrnuje několik hierarchicky uspořádaných úrovní. Nejníže se nacházejí stroje a specializované regulační prvky, které jsou řízené programovatelnými logickými automaty PLC. Na jejich činnost pak dohlížejí počítače, které monitorují stav technologického procesu, zpracovávají naměřená data, komunikují s operátory a podobně.
Dohlížecí úroveň
PLC
PLC
Řízení procesu Výkonné spínače
Regulace pohonů Ovládací panely Snímače
Snímače
Pohony
Technologický proces
Stroje, výrobky
Obr. 0-1 Počítače PC v øídících aplikacích Na nižších úrovních řízení pracují maximálně spolehlivé a robustní systémy. Naproti tomu, dohlížecí úroveň nevyžaduje většinou úkoly, které mají charakter časově kritických operací, a používají se tam převážně počítače PC vybavené některým operačním systémem Win32, zejména kvůli jejich nižší ceně a všestrannějšímu použití oproti jiným dokonalejším systémům, a také pro malé náklady na zaškolení obsluh, protože s nimi umí zacházet většina techniků. Znalost programování Windows se hodí nejen pro tvorbu vlastních aplikací pro dohlížecí úroveň, ale i v případě nasazení profesionálních vizualizačních programů, jako RSView nebo WinCC. I ty občas vyžadují realizaci některých speciálních operací pomocí externích aplikací anebo dynamických knihoven. Kromě toho dovolují napojit se na ně vhodným prostředkem pro interní přenos dat, zpravidla DDE protokolem, a pak lze i v jiných programech využívat jejich možností, zejména pro sběr dat z PLC. Poznámka: Možná vás udivilo, že se programování Windows se přednáší v předmětu Programovací jazyky pro řízení. Rozpor obsahu a názvu zapříčinila skutečnost, že zmíněný předmět vznikl v době, když se v něm probíral Asembler a jazyk C pro řízení v MS-DOSu, a po inovaci jeho obsahu se z řady důvodů muselo zachovat původní pojmenování. Nicméně si myslím, že kombinace starého názvu a nové náplně patří k těm lepším variantám.
7
Programovací prostředí pro Windows a příklady Osobně považuji za nejlepší vývojový nástroj pro běžné uživatele Borland C++ Builder. V něm se plánují i cvičení z předmětu, jakmile to dovolí technické možnosti katedrální sítě. Borland C++ Builder 3.0 pracuje ve Win32 s doporučenými 32 MB paměti (minimum je 24 MB). Používá jazyk C++ kompatibilní s vývojovým prostředím Borland C++ 4.5, na němž něm byla odladěná většina příkladů ve skriptu. K této volbě došlo z ryze pedagogických důvodů; Borland C++ 4.5 lze provozovat ve Win32 i ve Win16 a stačí mu 16 MB RAM. Nástupcem Borland C++ 4.5 je Borland C++ 5.0, který vyžaduje Win32 a představuje finální produkt řady Borland C++. Poté se firma Borland, dnes se jmenující INPRISE Corporation, zaměřila vývoj v oboru nástrojů pro jazyk C++ na Borland C++ Builder. Veškeré příklady ve skriptu, které nepotřebují grafické prostředí Windows, lze ve Win32 vyzkoušet v Borland C++ Builder anebo v Borland C++ jako Win32 Console aplikace, v nichž se píše stylem podobným C++ programům pro MS-DOS, až na některé výjimky (více o Win32 Console aplikacích v kapitole 4.6 na str. 113). Borland C++ 4.5 dále nabízí pro Win16 možnost vytvořit programů typu Easy Windows jako částečnou náhradu MS-DOS aplikací. Zdrojové kódy rozsáhlejších příkladů lze získat na již uvedené WEB stránce: http://novell.felk.cvut.cz/~susta.control/skripta/ resp. http://147.32.87.2/~susta.control/skripta/ Tam se také umístím i dodatky a případné korektury. Příklady budou po úpravách přeložitelné i ve Visual C++. To používá odlišné názvy pro některé knihovní funkce a uplatňuje jiná pravidla pro přetypování pointrů a pro obsluhu výjimek (údaje brané podle verze Visual C++ 5.0). Na některé takové rozdíly se poukazuje v textu. Visual C++ 5.0 potřebuje Win32 a nabízí výrazně lepší podporu pro psaní programu oproti Borland C++ 4.5 i 5.0, avšak navzdory svému názvu obsahuje méně visual rysů než Borland C++ Builder. Výklad objektového přístupu k Windows začíná příkladem vytvořeným v objektové knihovně OWL, Object Window Library, užívané Borland C++ 4.5 a 5.0. Ta byla navržená pro klasické psaní programu bez pomocných generačních nástrojů a vyznačuje se krásnou ryze objektovou architekturou, na níž se dají dobře ukázat veškeré souvislosti. V kapitole 6.3 se pak, pomocí téhož příkladu, demonstrují rozdíly řešení ve Visual C++ 5.0 knihovně MFC, Microsoft Foundation Classes. Ta má některé rysy shodné s OWL, avšak zdrojové kódy nejsou mezi OWL a MFC rozhodně přenositelné, protože obě knihovny vycházejí z jiných principů. Nakonec se tentýž příklad v kapitole 7.5 transformuje do Borland C++ Builder knihovny VCL, Visual Component Library. Její zařazení až nakonec vyplývá ze skutečnosti, že se nehodí pro první seznámení s principy objektového C++ přístupu; mimo jiné i kvůli svému určení pro RAD, Rapid Application Development, což znamená rychlý vývoj aplikací, při kterém se část zdrojového kódu vytváří interaktivně a programovací prostředí ho generuje v poloautomatickém režimu. Borland C++ Builder 3.0 verze Professional dovoluje používat všechny tři objektové knihovny (VCL, OWL i MFC) a dále zahrnuje i Borland C++ 5.0.
Jazyková poznámka Terminologie programování se opírá o pojmy založené na angličtině. Nesnažil jsem se k nim většinou hledat české ekvivalenty, protože neznám jediný počeštěný překladač. Původní termíny používám bez překladu a volně je vkládám do českého textu. Ty nejběžnější z nich, například pointer, dokonce skloňuji jako česká slova - pointru, pointrem. Dobře vím, že právě ke slovu pointer existuje i výstižný český přepis ukazatel, jenomže v češtině se ke slovu ukazatel váže řada významů - třeba ukazatel směru na silnici, ukazatel hladiny vody, ukazatel nehodovosti; zatímco termín pointer vymezuje zcela konkrétní pojem z programování. Doufám, že mi tuto troufalost odpustí čtenáři dbající na čistotu českého jazyka. Na omluvu si dovoluji poukázat na skutečnost, že nemůže existovat žádné vzájemně jednoznačné zobrazení anglické terminologie na česká slova, neboť čeština obsahuje necelých 50 tisíc slovních základů, zatímco angličtina jich má údajně kolem 900 tisíc, především díky tomu, že se s ní jednak 8
mluví ve větší části světa, a proto zahrnuje víc místních pojmů, a jednak potřebuje více slov, aby nahradila chybějící tvarosloví. Ať se nám to líbí nebo ne, dnes se angličtina stala pro programátory stejným esperantem jako kdysi latina pro doktory anebo přírodovědce. V těchto oborech se už celá staletí běžně používají cizojazyčné termíny a dává se jim přednost pro jejich významovou jednoznačnost i v případech, když existují české názvy. Při psaní skripta jsem pouze převzal jejich praxi. Zastáncům ryzosti češtiny si dovoluji předložit malý kviz: A. Co říká věta "Nelze pracovat ve spraženém režimu" ? (hlášení českých Windows 95) B. Co to je "Rolovací tlačítko?" ? (A. Limpouch: X Windows systém ) C. Co znamená: "Ukazatel na nic." ? (diplomová práce studenta R.L.)
Posuny textů v obrázcích Ve vytištěných obrázcích se občas vyskytují posuny části českého textu před prvním znakem s háčkem čí kroužkem, např. "klí čové slovo " . Přes velkou snahu se mi nepodařilo objevit příčinu této závady - na obrazovce se neprojevuje. Způsobuje ji zřejmě nekompatibilita mezi instalacemi programů Word (95 i 97) a síťových laserových tiskáren HP. Čtenářům se za tento nedostatek předem omlouvám. Jeho odstranění by vyžadovalo překreslení většiny obrázků.
9
1 Opakování některých důležitých pojmů jazyka C a C++ V této kapitole se probírají pojmy jazyka C a C++, které podle mých zkušeností působí studentům 3. ročníku obtíže a jsou nutné pro pochopení dalších částí. Jedná se především o rozklad zdrojového textu programu do více souborů a s tím související atributy proměnných, důležité zejména pro objektové programy - linkage, scope a visibility. V druhé půlce kapitoly se pak detailně rozebírají pointry a práce s pamětí. Současně s nimi se vysvětlují paměťové modely a virtuální mapování paměti, mající mimořádný význam pro Win32.
1.1 Něco málo o historii jazyka C Jazyk C vytvořil Dennis Richtie jako programovací nástroj pro psaní operačních systémů. Jeho varianty popořadě označoval písmeny A, B, C. Poslední již vyhovovala a v roce 1973 byla použita pro operační systém UNIX. Název UNIX vymyslel Brian Kernigham v roce 1969 pro zjednodušenou verzi operačního systému, kterou vyvinul ze složitého MULTICSu, produktu Bellových laboratoří (dnešní AT&T) a firem GE a MIT. Společně s Dennisem Richtie přepsal zdrojové kódy nejnovější verze UNIXu (UNIX 6) z asembleru do jazyka C. Oba partneři později vydali společnou knihu „Jazyk C a systém UNIX“, v níž nový programovací nástroj představili veřejnosti. Od té doby prodělal jazyk C bouřlivý vývoj a v mnoha rysech se hodně liší od původní syntaxe, někdy označované jako norma Kernigham & Richtie, takže zmíněná kniha obou autorů má dnes už jenom historický význam. Modernější verzi jazyka popisuje norma ANSI, (American National Standards Institute), nazývaná ANSI C, a s tou je pak shora kompatibilní verze C++, kterou vytvořila společnost AT&T doplnění objektů, norma ANSI C++. Ta nás bude zajímat nejvíc. Jazyk C nese pečeť nástroje vhodného pro tvorbu operačních systémů a jiných náročných programů. Neobsahuje přílišná omezení, která by svazovala styl programátora a komplikovala mu práci, a nabízí řadu mocných nástrojů dovolujících vytvářet efektivní programy. To je samozřejmě dvojsečná zbraň. V jazyce C lze vytvořit naprosto nepřehledný kód, jehož rozluštění zabere desítky hodin. Stačí enormní využití maker s parametry, instrukcí goto a pointrů. Na druhou stranu, nic nenutí programátory, aby používali podobné divoké konstrukce. Špatný program je především chybou jeho autora, nikoliv programovacího jazyka. Opačný pohled zaujímají programovací jazyky vybavené silnými omezeními. Vzniká tak věčný spor - dát programátorovi svobodu nebo mu ji sebrat? Podobné diskuse se dají přirovnat k rozhodování mezi silným policejním režimem a volností. Pevná kontrola zaručuje, že se nikdo neproviní proti panujícímu řádu, a zvyšuje obecnou bezpečnost. Má však nevýhody všech policejních vlád. Pokud náhodou potřebujete udělat něco, co se příčí stanoveným pravidlům, narazíte na desítky zdí a zjistíte, že neexistuje jediná cesta jak to provést jednoduše a musejí se volit krkolomné okliky přes ostnaté dráty. Naproti tomu, jazyk C nabízí svobodu při tvorbě kódu a spoléhá na vás, že ji nebudete zneužívat. Patří mezi nástroje pro profesionály a začátečníci s ním musejí zacházet opatrně. Budou-li uvážlivě využívat jeho možnosti, napíší přehledný a současně velmi bezpečný kód, i když v něm budou pracovat s pointry, skoky, makry a dalšími specialitami. Ne nadarmo se jazyk C drží v čele pozornosti programátorů už přes čtvrt století. Za tu dobu odolal řadě útoků na volnost, kterou poskytuje, a svou syntaxí inspiroval mnoho jiných jazyků. Vsuvka: Odpovědi na kviz na předchozí stránce: A: Nelze pracovat on-line. B: Scroll bar tlačítko. C: Pointer typu void.
10
1.2 Atributy datových objektů Mezi datové objekty se v jazyce C++ řadí všechny proměnné, konstanty, pole, struktury, třídy. Každý objekt má svůj identifikátor a k němu přidružené atributy. Vzhledem k tomu, že objektové programování se opírá, jak jeho název naznačuje, především o objekty, je velmi důležité porozumět jejich vlastnostem. Identifikátor datového objektu dostává nejméně dva atributy - datový typ (například int, long, double apod.) a storage class, mající význam způsobu uložení. Překladač určuje hodnoty atributů přidělených identifikátoru na základě implicitních nebo explicitních vlastností objektu, tj. není-li nic uvedeno, použijí se implicitní, čili výchozí hodnoty, v opačném případě se uplatní explicitně specifikovaná vlastnost. Datový typ určuje kolik paměti dostane objekt přiděleno a jak se bude interpretovat bitový obsah této paměti, čili chování operátorů aplikovaných na objekt. Například: char c = 0x80; // implicitně signed char ( prefix 0x označuje hexadecimální číslo) unsigned uc = 0x80; if( c<0 ) c=0; // podmínka c<0 splněna, a proto se c přiřadí 0; if( uc<0 ) uc=0; // podmínka nesplněna, uc zůstane rovné 0x80; Každý identifikátor, c i uc, dostal při překladu přidělené umístění v paměti o velikosti jednoho bytu a obsahy obou bytů byly inicializované na hodnoty s bitově totožným obsahem. Proměnná uc má ale datový typ unsigned char, zatímco proměnná c je signed char. Klíčové slovo signed doplnil překladač implicitně. Všimněte si, že změna datového typu modifikovala v tomto případě pouze chování operátoru porovnání. Ten chápal obsah bytu c jako číslo -128 a naproti tomu s bytem přiděleným uc pracoval jako s číslem 128. Velikost paměti přidělené datovému objektu lze zjistit operátorem sizeof, který se vyhodnocuje během překladu - sizeof(uc) nebo sizeof uc; oba výrazy dávají hodnotu 1. Atribut storage class diktuje, kde bude umístěný objekt svázaný s identifikátorem, jestli v datovém segmentu, v registru, ve volné paměti (angl. heap) či na programovém zásobníku (angl. stack). Dále určuje rozsah platnosti - scope, dobu existence - duration a také linkage, čili dostup z dalších modulů programu, o nichž se bude hovořit v dalších odstavcích. Vlastnosti scope a linkage se kromě datových objektů týkají také identifikátorů funkcí.
1.2a
Scope (rozsah platnosti)
Scope určuje, v jaké části zdrojového kódu programu se smí používat identifikátor odkazující na nějaký objekt. Nejčastěji se uplatňuje pět rozličných scope: Lokální scope, neboli také block scope, se týká identifikátorů deklarovaných uvnitř příkazového bloku, tj. uvnitř { }. Jejich scope začíná bodem deklarace a končí koncem příkazového bloku, tj. odpovídající }. Objekty s tímto scope se často nazývají lokálními. Lokální scope dostávají rovněž formální parametry definic funkcí - scope parametrů začíná a končí s příkazovým blokem těla funkce. Globální scope, neboli také file scope, mají všechny identifikátory definované vně všech bloků a tříd. Jejich scope začíná bodem deklarace, stejně jako lokálních objektů, ale na rozdíl od nich končí až s koncem souboru. Datové objekty s tímto scope se často nazývají globálními. Toto scope dostávají veškeré identifikátory funkcí. Scope funkce mají pouze návěští instrukce goto. Jejich scope ohraničuje příkazový blok těla funkce. Lze na ně odkazovat kdekoliv uvnitř funkce a to i před bodem jejich deklarace.
Datové typy bývají implicitně signed. Pro char lze změnit implicitní nastavení na unsigned buď v podmínkách (options) překladače nebo příkazem #pragma (u Borland C++ a C++ Builder #pragma option -K). Řada dalších příkazů v C++ (třeba new, delete, return) dovoluje obě formy zápisu - se závorkami jako volání funkce nebo ve tvaru operátoru - například return(2); nebo return 2; Moderní podoba jazyka C++ dává přednost operátorům, tj. return 2; a jen sizeof se většinou nechává ve tvaru funkce.
11
Scope funkčního prototypu se týká výhradně formálních parametrů uvedených v prototypech funkcí, tj. hlavičce funkce bez těla (blíže v kap. 1.21.2d1.2d.2). Platnost formálních parametrů se za této situace omezuje pouze na příkaz deklarace prototypu. Class scope, čili platnost třídy, mají všechny identifikátory členů struktur (struct) nebo tříd (class). Podobně jako u návěští, jejich scope začíná už s úvodní { závorkou bloku deklarací a končí s koncovou } závorkou, následkem čehož u struktur a tříd nezáleží na pořadí deklarací jejich členů. Této vlastnosti se budeme podrobněji věnovat až v kapitolách 2 a 3. Následující příklad ukazuje čtyři typy scope - lokální, globální, funkce a funkčního prototypu. int iglob; // Začátek globálního scope identifikátoru proměnné iglob. void funkce2(int plokal2); /* Prototyp funkce: pro funkce2 - začátek globálního scope; pro plokal2 - začátek i konec scope funkčního prototypu. */ void funkce1(int plokal1) // Začátek globálního scope identifikátoru funkce1. { /* Začátek lokálního scope formálního parametru plokal1 a současně začátek scope funkce pro návěští Label. */ int ilokal; // Začátek lokálního scope identifikátoru ilokal. goto Label; // Příkaz ve scope návěstí Label, a proto lze na Label odkazovat. Label: ilokal = 2*plokal1; iglob = plokal1; funkce2(1); // Příkaz ve scope identifikátoru funkce2. /*!!*/ plokal2 = 0; // CHYBA - nezná se plokal2, nemá platný scope. } // Končí scope identifikátorů plokal1, ilokal a Label. void funkce2(int plokal2) // Doplnění prototypu o tělo funkce - scope funkce2 se tím nemění. { // Začátek lokálního scope identifikátoru parametru plokal2. iglob = plokal2; } // Konec lokálního scope identifikátoru parametru plokal2. //----- Konec souboru → konec scope iglob, funkce1 a funkce2 Důležitou vlastností spojenou se scope je příslušnost identifikátoru do name space. To specifikuje logickou množinu jmen a má význam tabulky, do níž si překladač ukládá jednotlivé identifikátory. Pro každý name space platí jiná pravidla specifikující scope, uvnitř něhož musí příslušný identifikátor zůstat jedinečný. Ukažme si to na příkladu. Nelze například napsat: int iglob; // name space 4 void iglob(void); // name space 4 → CHYBA! Duplicitní deklarace avšak lze uvést: void ident(void) // name space 4 { // příkazový blok struct ident // name space 2 { int ident; } // name space 3 ident; // name space 4 goto ident; ident: // name space 1 ident.ident = 1; } neboť každý identifikátor definovaný uvnitř příkazového bloku funkce ident patří do jiného name space, čili do jiné tabulky symbolů, kterou si překladač vytváří. Jazyk C používá čtyři name space s těmito pravidly jedinečnosti:
12
1. návěští goto. Ta musejí být jedinečná uvnitř celého příkazového bloku funkce, v němž jsou deklarovaná. Jména mohou být stejná, jen pokud se nacházejí v různých funkcích. 2. tagy, neboli jména přidělená strukturám (struct), třídám (class), unionům (union) a výčtovým typům (enum). Ta musejí být unikátní pouze uvnitř bloku, v němž jsou definovaná. Globálně definované tagy musí být jedinečné v rámci všech globálních tagù. 3. členové struktur, tříd a unionů. Ty musejí být unikátní uvnitř svojí struktury, třídy, či unionu, v němž jsou definované. Nekladou se ale žádná omezení na výskyt shodných jmen, pokud leží ve vzájemně různých strukturách, třídách a unionech. 4. identifikátory proměnných, funkcí, typedef příkazů a členové příkazů enum. Ty musejí být jedinečné pouze uvnitř scope, v němž byly deklarované. Pokud mají externí linkage (pojem bude vysvětlen o pár stran dále), pak musejí být jedinečné v rámci všech identifikátorů s externím linkage. Rozčlenění identifikátorů na různé name spaces, zjednodušuje psaní programů a zvyšuje bezpečnost kódu. Například skutečnost, že návěští goto se ukládají do samostatného name space, zaručuje jejich naprostou nezaměnitelnost například s identifikátory pointrů nebo funkcí. Stejný efekt se projeví i v případě tagù a identifikátorů členů struktur a podobně. Poznámka1: Norma C++ zná i šestý scope, mající praktický význam jen pro příkazy for. Uvnitř nich lze definovat proměnnou se scope podmínky. To začíná bodem deklarace proměnné a končí s koncem příkazu for. Lze tak vytvořit třeba dočasnou proměnnou pro parametr cyklu. Tento rys ale nepodporují všechny překladače. Borland C++ a Visual C++ sice dovolují podobné definice, ale proměnným vytvořeným v příkazu for dávají lokální scope příkazového bloku, v němž se for nachází. Scope podmínky umožňuje Borland C++ Builder, nabízející dokonce obě varianty, které se přepínají parametrem překladače /Vd. void fibon(void) { int nminus1mem, nminus1=0; for( // Začátek příkazu for. int n=1; // Vytvoření pomocné proměnné n → začátek jejího scope. n < 10; ) { printf("%d, ", n); // Tisk: 1, 1, 2, 3, 5, 8, nminus1mem = nminus1; nminus1 = n; n += nminus1mem; } // Končí příkaz for a při nastavení /Vd- zde končí i scope n. printf("%d", n); // Tisk 13 při nastavení /Vd+; při /Vd- chyba nedefinované n. } // Teprve tady končí scope n, je-li zadáno /Vd+. V Borland C++ Builder je výchozí hodnota nastavení /Vd- (nezatržené pole v C++ konfiguraci: Project → Options → C++ → Don't restrict for loop scope, tj. proměnné definované v příkazu for mají scope podmínky). Borland C++ a Visual C++ pracují jakoby měly pevně zadané /Vd+. Poznámka2: Syntaxe C++ připouští vytvoření proměnné i uvnitř podmíněných příkazů if, switch a while, které rovněž mají scope podmínky. Avšak tata možnost nemá širší uplatnění.
1.2b
Viditelnost identifikátorů a operátor ::
Nejvýznamnějším důsledkem scope je visibility, čili viditelnost identifikátorů, která hraje klíčovou roli při vytváření objektů a dostupu na jejich členy. Má pouze dvě hodnoty, viditelné nebo skryté (hidden). Uvažme příklad: int ivar; // globální proměnná ivar void fce(void) { int ivar; // lokálního proměnná ivar překryla globální ivar ivar = 1; // ukládáme do lokální ivar }
13
Uvnitř příkazového bloku funkce se definuje proměnná ivar se stejným jménem jako má již existující globální proměnná ivar. Identifikátory proměnných patří do čtvrtého name space a ten od nás žádá jedinečnost názvu pouze uvnitř scope, kde došlo k deklaraci. Globální ivar má globální scope. Jakmile jsme se dostali do lokálního scope příkazového bloku funkce, globální ivar má v něm sice stále platný scope, ale nejsme už v tom scope, v němž byla deklarovaná. Smíme proto použít tentýž identifikátor pro definici lokální proměnné. Tím budou ve scope současně dva identifikátory ivar. Překladač si z nich implicitně vezme ten, která má nebližší scope, a proto se v příkazu "ivar = 1;" bude pracovat s lokální ivar. Základní důvodem k překrytí je zjednodušení práce ve velkých programech. Potřebujeme-li uvnitř příkazového bloku definovat nějakou proměnnou, tak si ji vytvoříme a nemusíme se ohlížet na tisíce předchozích deklarací. V objektech navíc podobné překrývání umožňuje měnit upravit chování odvozených tříd. Globální ivar není po definici lokální ivar viditelná, nicméně stále má platný scope a lze na ni dostoupit pomocí speciálního operátoru :: určujícího scope, ve kterém se má identifikátor hledat. To znamená, že s globálními proměnnými můžeme v jazyce C pracovat vždycky, i když jsou zakryté. int ivar; // globální proměnná ivar void fce(void) { int ivar; // lokálního proměnná ivar překryla globální ivar ivar = 1; // ukládáme do lokální ivar ::ivar = 0; // ukládáme do globální ivar } Co se však stane, když příklad trochu pozměníme. Do funkce vložíme další příkazový blok, ohraničený { }, v němž definujeme jinou lokální proměnnou, ale opět s identifikátorem ivar. Tu budeme pro rozlišení nazývat ivar /*2*/. Uvnitř druhého příkazového bloku budou tak existovat dva skryté identifikátory, globální ivar a lokální ivar /*1*/. Na globální ivar můžeme kdykoliv dostoupit pomocí operátoru ::, avšak lokální ivar /*1*/ uvnitř druhého příkazového bloku je naprosto nedostupná a v jazyce C neexistuje jediná metodu, jak s ní pracovat. To bude možné teprve až po skončení vloženého příkazového bloku a tím scope ivar /*2*/. int ivar; // globální proměnná ivar void fce(void) { int ivar; // lokálního proměnná ivar překryla globální ivar ivar = 1; // ukládáme do lokální ivar /*1*/ { // otevřen vnořený příkazový blok int ivar; // druhá lokálního proměnná ivar /*2*/ překryla lokální ivar /*1*/ ivar = 2; // ukládáme do lokální ivar /*2*/ ::ivar = 0; // ukládáme do globální ivar } // kolec vnořeného příkazového bloku ivar = 1; // ukládáme opět do lokální ivar /*1*/ } Poznámka: Scope operátor :: má největší význam v objektech. Můžeme jím zadat hledání jména ve scope příslušné třídy, uvedeme-li před něj její jméno. Operátor ale nedovoluje použít k určení scope identifikátor funkce. Zápis ve formě fce::ivar není přípustný, a proto mimo objekty lze scope operátor :: použít jen ke specifikaci globální proměnné. Ukažme si uplatnění operátoru :: a viditelnosti na následujícím příkladu, který demonstruje chování viditelnosti a scope jednotlivých proměnných; obé bude důležité pro objekty. (Následující příklad slouží pouze pro demonstraci vlastností scope a viditelnosti. Rozhodně není vzorem pro psaní programů. V nich vytvářejte, pokud možno, vždy unikátní a významově výstižná jména proměnných. Mějte na paměti, že překrývání identifikátorů je pomůckou pro usnadnění práce a třeba ho užívat jen tam, kde má skutečně smysl.) 14
struct A { int A; int B; };
// konec scope členů A a B
A A; int B;
// deklarace globální proměnné typu struktura A, alternativní zápis: struct A A; // globální proměnná B
void F1(int A) { A B;
// globální funkce F1, s lokálním parametrem A **********************
// definice uspořádání struktury, tzv. "tag" // začátek scope obou členů A a B // identifikátory členů jsou v jiném name space než tag A
F1=A;
/* chyba - A není typ, struct A je sice viditelná, ale tvar příkazu nedovoluje rozlišit struct A od parametru A */ /* V pořádku, když slovem stuct zdůrazněno, že A je struktura. Předřazením struct určíme, že první A patří do name space struktur. Uvedení struct není nutné, provádí se pouze v případech, kdy hrozí záměna. */ /* deklarace lokální proměnné F1, funkce F1() je globální proměnná a lze ji zde překrýt lokální deklaraci */ // do lokální F1 zápis lokálního A
F1(A); ::F1(A);
// chyba - globální deklarace F1 není viditelná // operátorem :: odkazuje na globální deklaraci F1
A.A = A; ::A.A = A;
// chyba: A není struktura // v pořádku, operátor :: přesměroval scope na globální proměnnou
B=A; B.B=A; ::B=A;
// chyba: globální int B není viditelné // zápis lokálního A do členu B lokální struktury B // uložení parametru A do globální proměnné B
::B=A.B; ::B=::A.B;
// chyba: globální struktura A není viditelná
struct A B; int F1;
int A;
// chyba: nelze překrýt identifikátor uvnitř příkazového bloku, v němž je definovaný
{
// vložený příkazový blok --------------------------------------------------------------------int A; /* deklarace lokální proměnné A ve vnořeném příkazového bloku. Parametr A funkce F1 se tím stal totálně nepřístupný. Nelze s nim pracovat. */ A=B.B; // zápis do A deklarovaného o řádek výše ::B=A; // uložení téhož A do globálního B ::B=::A.B; // práce s globálními proměnnými } // konec vnořeného příkazového bloku a A v něm definovaného ----------------------------
B.B=A; F2(A); } void F2(int A) { F1(A); F2(A); }
// parametr A funkce F1 se stal opět viditelný // chyba: F2 neexistuje, její scope ještě nezačal // konec F1 a s tím i všech lokálních proměnných ******************** // definice F2 a začátek scope jejího identifikátoru // volání F1 // volání F2
15
1.2c
Duration
Další vlastnost atributu storage class zvaná duration (trvání) má hodně společného se scope. Specifikuje periodu, během níž má objekt přidělené místo v paměti. Atribut dostávají výhradně objekty existující při běhu programu (run-time) a nevztahuje se na prvky, které vznikají pouze v době překladu (compile-time), jako třeba na definice typů typedef. Duration pro run-time objekty nabývá tří hodnot: • statické (static) - tyto objekty vznikají před spuštěním programu a ruší se po jeho skončení. Statické duration mají všechny globální objekty. • automatické (automatic) - takové objekty se vytvářejí a ruší během běhu programu automaticky na zásobníku (stack), eventuálně v registrech procesoru. Automatické duration se přiděluje jako výchozí (default) všem lokálním objektům. Jejich konkrétní umístění, zá! sobník či registr, závisí na nastavených podmínkách a algoritmu překladu. • dynamické (dynamic) - tyto objekty se vytvářejí se a ruší speciálními funkcemi při běhu programu, např. operacemi new a delete. Duration se vyznačuje silně předdefinovaným charakterem a lze ji změnit pouze u lokálních proměnných. Ty mají primárně automatické duration, které lze upravit na statické duration klíčovým slovem static, například: int glob; // globální proměnná glob se statickým duration void CitacPP(void) { int ivar; // lokální proměnná s automatickým duration static int citac; // lokálního proměnná se statickým duration svar++; // při každém zavolání funkce načteme čítač o 1 ivar = citac; } // konec duration ivar a tím i znehodnocení obsahu ivar Klíčové slovo static změnilo umístění proměnné citac. Ta se nyní nevytváří na zásobníku při každém zavolání podprogramu jako její kolegyně ivar, ale už ve chvíli spouštění programu. Existuje trvale v datové paměti podobně jako globální proměnná glob a stejně jako ta zanikne až se skončením celého programu; statická lokální proměnná se od globální proměnné liší jen svým lokálním scope. Díky tomu bude citac při každém zavolání podprogramu CitacPP obsahovat předchozí do něho uloženou hodnotu. Naproti tomu ivar, lokální proměnná s automatickým duration, bude mít nedefinovaný obsah při každém novém zavolání funkce CitacPP. Pozor! Klíčové slovo static se používá také pro globální proměnné, ale má pro ně naprosto odlišný význam. Nemění jejich duration ale linkage!
1.2d
Linkage a program ve více souborech
Atribut linkage se vztahuje k činnosti programu linker. Má význam pouze pro globální objekty a identifikátory funkcí. Je o něco složitější než ostatní atributy, ale o to důležitější. Vysvětlíme ho na příkladu programu, rozděleného do více zdrojových souborů, čili do několika modulů, protože tehdy má linkage největší význam. Každý program lze vytvořit jako jeden jednolitý textový soubor, ale takové řešení nepřináší u větších projektů žádné výhody. Výsledek je nepřehledný a špatně se s ním manipuluje. Jazyk C se proto opírá o strukturu zdrojového programu uloženého ve více souborech, přičemž se předpokládá, že jednotlivé dílčí části programu mohou odkazovat na identifikátory s globálním scope, definované v druhých zdrojových souborech. Na objekty s jiným scope než s globálním se odkazy nepřipouštějí. Uvažujme například, že potřebujeme číst datum z klávesnice a převést ho na trojici čísel den, měsíc, rok. Program nazveme třeba multi. !
Dávání proměnných do registrů procesoru lze zakázat podmínkami překladu, čímž se usnadní ladění programů. Je-li povolené, pak lze výběr proměnné určené pro uložení v registru ovlivnit C klíčovým slovem register.
16
#include <stdio.h> // vložení deklarací stdin, fgets(), printf() #include
// vložení deklarace makra isdigit(c) #define MAXBUF 256 // maximální velikost řádky #define TRUE 1 // konstanta struct RADKA // struktura pro uložení načtené řádky { char pozice; // aktuální pozice čteni v buf char buf[MAXBUF]; // paměť znaků řádky } radka; // definice proměnné radka typu RADKA int ok=0; // příznak vše v pořádku inline void CtiRadku(void) { fgets(radka.buf,MAXBUF,stdin); // čtení určeného znaků z klávesnice radka.pozice=0; // nulování pozice čteni } char CtiZnak(void) // čtení jednoho znaku z proměnné radka { char c=radka.buf[radka.pozice]; // čtení znaku, který je na řadě if(c>0) radka.pozice++; // není-li konec řádky, posun pozice return c; }; int CtiCislo(int max, int min=1) // parametr min má výchozí hodnotu 1 { char c; int cislo=0; while( ' '==(c=CtiZnak()) ); // vynecháme mezery if( isdigit(c) ) // ? znak je dekadická číslice { cislo = c - '0'; // převedeme ASCII znak na číslo c=CtiZnak(); // čteme další znak if( isdigit(c) ) // je-li číslicí, přidáme ho k číslo { cislo = cislo*10 + c-'0'; c=CtiZnak(); // vynechání oddělovače if( isdigit(c) ) ok = !TRUE; //chyba, pokud má číslo víc jak dvě číslice } } if( cislo<min || cislo>max ) ok=!TRUE; // chyba, je-li číslo mimo povolený rozsah return cislo; } char CtiZnak1(void) { CtiRadku(); return radka.buf[0]; } // vstup znaku odpovědi ano, ne int den,mesic,rok; // uložení data void main(void) { do // ošetření výskytu chyby { ok=TRUE; // nastavení příznaku pokračování while(ok==TRUE) // je-li vše v pořádku, převáděj datum { printf("Zadej datum ve tvaru DD.MM.RR (RR=90..99) : "); CtiRadku(); // čteni řádky z klávesnice den = CtiCislo(31); mesic = CtiCislo(12); // ve funkci CtiCislo bude parametr min = 1 rok = CtiCislo(99,90); // parametr min=90 if(ok==TRUE) // měl text správný formát? printf("Datum: %2d.%2d.%2d\n", den, mesic, rok ); // tiskni datum else printf("Text: %s\n", radka.buf); // detekována chyba } printf("Chyba - konec programu (a/n) ? "); } while( CtiZnak1()=='n' ); // pokračuj, není-li požadavek ukončit práci } 17
Funkce main obsahuje dvě do sebe vnořené smyčky, vnější typu do..while ošetřující výskyt chyby a vnitřní typu while..do provádějící vlastní převod zadaného datumu na trojici čísel den, měsíc a rok a výpis výsledku. Vnitřní smyčku zahajuje funkce CtiRadku vytvořená stylem inline, což má za následek, že se funkce nepřekládá jako volání podprogramu, ale přímým vložením svých operací do kódu, tedy stejně tak, jako kdyby se místo každého použití funkce napsaly všechny její příkazy. Funkce CtiRadku načte řádku z klávesnice, čemuž odpovídá v jazyce C čtení souboru stdin " a výsledek uloží do proměnné radka typu struct RADKA, avšak v počtu nejvýše MAXBUF-1 znaků. Nakonec nastaví ukazatel pozice čtení, tj. proměnnou radka.pozice na 0. Poté se třikrát zavolá funkce CtiCislo převádějící jednu až dvě číslice z radka.buf a kontrolující správný rozsah výsledku podle zadaných parametrů max, min. V prvních dvou volání je vynechaný parametr min a překladač místo něho použije hodnotu 1, uvedenou v hlavičce funkce jako výchozí (default). Pokud text neobsahoval chyby, zobrazí se přečtené datum a pokračuje se v převodu. Detekovala-li funkce CtiRadku chybu, pak nastavila proměnnou ok na 0, což má za následek ukončení vnitřní smyčky while..do a vypsání dotazu na ukončení programu. Poté se zavolá funkce CtiZnak1(), která přečte odpověď. Začíná-li odpověď znakem ´n´, nastaví se ok na TRUE a pokračuje se v převádění, jinak se ukončí vnější smyčka do..while a tím i celý program. Rozložme nyní program do dvou zdrojových souborů. Náš krátký příklad sice takové řešení nevyžaduje, ale představme si, že jeho zdrojový text nabobtnal o další operace a stal se nepřehledným. Vydělíme z něho pomocné funkce a ty umístíme do zvláštního souboru, který pojmenujeme jako multi2. Umístíme tam také definici proměnné ok a dvě funkce CtiZnak() a CtiCislo(). Zbylé operace necháme v souboru, který nazveme multi1. Podívejme se napřed data programu multi - na jejich zdrojové kódy a na překlad při dělení programu do více souborů. Teprve poté se budeme věnovat funkcím. 1.2d.1 Data ve souborech Multi1 a Multi2 V souboru multi2 se definuje příznak specifikující, že zpracování proběhlo bez chyby: int ok = !TRUE; Proměnnou ok nastavujeme ale i v souboru multi1, a proto musíme při jeho kompilaci poskytnout překladači informaci, že identifikátor ok znamená proměnnou typu integer definovanou někde jinde. Nemůžeme ovšem v multi1 opakovat příkaz int ok; , protože ten znamená vytvoření proměnné a my nechceme, aby v multi1 existovala další proměnná ok, ale přejeme si, aby se používala ok definovaná v multi2. Naším úmyslem je pouze dát informaci o identifikátoru a datovém typu, který se k němu váže. Toho dosáhneme uvedením klíčového slova extern před deklarací proměnné a současně vynecháním inicializace, neboť proměnnou ok v multi1 nevytváříme a tudíž nemáme ani co inicializovat: extern int ok; Podíváme-li se na to obecně, pak vytvoření jakéhokoliv datového objektu (proměnné, konstanty, pole, struktury) lze rozčlenit na tři etapy: • deklaraci - podání informace o identifikátoru a datovém typu, • definici- žádost o přidělení paměti objektu, • inicializaci - naplnění paměti objektu výchozími hodnotami. Řada C příkazů sdružuje kroků, například int cislo; zahrnuje deklaraci s definicí; podobně příkaz int cislo = 5; provádí všechny tři kroky naráz. Pořadí etap vytvoření objektu nelze změnit, ale nemusejí se nutně vykonat všechny a najednou. Často se používají pouhé deklarace, třeba v souborech vkládaných příkazy #include - ty obsahují stovky deklarací prvků knihovnách
"
Pro zapomětlivé: C programy primárně určené textový vstup a výstup, jako např. Win32 Console aplikace, otevírají při svém spuštění soubor stdin pro vstup z klávesnice a dva soubory stdout a stderr pro výstup na textovou obrazovku. Jedná se přesměrování I/O zařízení známé z UNIXu.
18
funkcí a dat, z nichž se v programu využije obvykle pouhý zlomek. Povoluje se též uvést napřed jenom deklaraci a tu někde později rozšířit o definici a případně i o inicializaci. Program lze zpravidla rozdělit na dvě části - na deklarace a na definice s inicializacemi proměnných. Inicializaci lze dále oddělit a provádět ji v kódu programu, ale podobné řešení už nepřináší větší výhody. Pro rozčlenění programu do více souborů postačuje roztržení na dva uvedené bloky. Každý zdrojový soubor C++ programu lze transformovat na dva dílčí soubory: • header, či též include soubor - obvyklá přípona *.h - kde budou soustředěné deklarace všech prvků, které tento blok programu nabízí jiným částem k používání. • zbytek programu - pro tento soubor se nezavádí žádné speciální jméno a zpravidla se nazývá jen program. Obsahuje příkazy pro vložení header souborů, dále deklarace používané pouze programem tohoto souboru a všechny v něm prováděné definice. Jeho obvyklá přípona bývá *.CPP pro C++ programy a C pro ANSI C programy. Části multi1 a multi2 našeho programu budou uložené celkem ve čtyřech textových souborech multi1.h, multi1.cpp, multi2.h a multi2.cpp. Jazyk C nepoužívá pro zápis konstant pouze datové objekty. Jeho specialitu představuje preprocesor, který předchází vlastní překlad a kromě jiného nahrazuje v textu symboly definované direktivami #define za určené řetězce. Symboly preprocesoru tvoří samostatnou množinu a mají charakter deklarací, protože existují pouze v době běhu preprocesoru a nepřiděluje se jim žádná paměť. Pokud si přejeme, aby se některé direktivy #define používaly i v jiných blocích, musíme je umístit do části header. V multi1.cpp máme definovanou konstantu TRUE, příkazem: #define TRUE 1 tu využíváme jednak v části multi1 pro nastavení hodnoty proměnné ok ( ok = TRUE; ) a jednak v části multi2, kde ok nastavujeme na logickou negaci TRUE (ok = !TRUE; ), a proto ji musíme umístit do multi1.h. S druhou konstantou MAXBUF se pracuje pouze v části multi1 a může zůstat definovaná jen v souboru multi1.cpp. K tomu, aby se dal náš program přeložit je potřeba zahrnout do souborů multi1.cpp a multi2.cpp dvojici direktiv #include, jimiž vložíme obsahy header souborů multi1.h a multi2.h: #include "multi1.h" #include "multi2.h" Použití "" u jmen specifikuje, že se soubory budou napřed hledat v okamžitém adresáři a teprve pak v systémových podadresářích INCLUDE, jejichž seznam se uvádí mezi parametry překladače, compiler options. Princip překladu dat našeho programu ukazuje Obr. 1-1, na němž jsou operace vložení souboru označené čerchovanými čarami. Program zpravidla používá i systémové datové objekty. Informace o nich se nacházejí v systémových header souborech, součásti překladače. V našem příkladu se jedná o definici stdin, proměnné typu ukazatel na soubor, která popisuje vstupní zařízení typu klávesnice a deklaruje se v stdio.h (standardní vstupy a výstupy); ten se vkládá do multi1.h. Část multi2 pou# žívá test na znak dekadické číslice isdigit popsaný formou makra s parametrem: #define isdigit(c) (c>=’0’ && c<=’9’) deklarovaného v systémovém header souboru ctype.h, který se musí vložit do multi2.cpp. Oba knihovní header soubory se vkládají direktivami preprocesoru #include, v nichž < > udávají, že se zadané soubory hledají jen v systémových podadresářích INCLUDE pøekladaèe: #include <stdio.h> #include Podívejme se teď, jak se bude náš program překládat. Začněme třeba od multi1.cpp. Překlad zahajuje preprocesor, který přečte multi1.cpp a zpracuje v něm všechny svoje direktivy. Výsledek zapíše do pomocného textového souboru, který nazveme třeba multi.tmp. Vytváří si přitom #
Přesný zápis makra isdigit závisí na použitém překladači.
19
pomocnou pracovní tabulku svých symbolů. V té se objeví i jméno "TRUE", jehož definice se nachází v multi1.h, a informace, že se má zaměnit za řetězec "1". Označení řetězec "1" je naprosto správné. Preprocesor funguje jako textový substituční nástroj a nezkoumá sémantický význam svých operací. Právě v tom leží jeho největší síla, neboť umožňuje vytvářet ojedinělé konstrukce nerealizovatelné na úrovni překladače. stdio.H FILE * stdin;
Pracovní tabulka symbolů preprocesoru TRUE = 1
MAXBUF=256 Symboly překladače
ok→ →extern int stdin → extern FILE *
stdio.LIB printf.obj fgets.obj stdin.obj
multi1.H
multi2.H
ctype.H
#define TRUE 1
extern int ok;
#define isdigit(c)
multi1.CPP
multi2.CPP
ok = TRUE;
int ok = !TRUE;
preprocesor
preprocesor
Pracovní tabulka symbolů preprocesoru TRUE = 1 isdigit(c) = ...
multi1.tmp
multi2.tmp
mezivýsledek
ok = 1;
náhrada isdigit + ok = ! 1 ;
C++ překladač
C++ překladač
mutli1.OBJ
multi2.OBJ
tabulka symbolů ok → extern int
tabulka symbolů ok → int
relativní kód ??? ← 1
relativní kód 0
Symboly překladače ok → int stdin →
extern FILE *
Tabulka linkeru multi1: extern int ok extern FILE * stdin multi2: int ok stdio.lib: FILE * stdin
Linker
multi.EXE Obr. 1-1 Princip překladu dat v ukázkovém příkladu MULTI Výsledkem činnosti preprocesoru bude jeden dlouhý textový soubor multi1.tmp, který vznikne fyzickým spojením souborů stdio.h, multi1.h, multi2.h a multi1.cpp a v nich nahrazením všech direktiv preprocesoru určenými řetězci. Příkaz ok=TRUE; se teď změnil na zápis ok=1; Soubor multi1.tmp zpracovává překladač jazyka C++. Ten ho převádí v několika krocích, minimálně ve dvou, některé překladače dokonce až v šesti (např. Watcom C++), a v každém kroku, nazývaném průchodem, provede část operací překladu. 20
Nejdřív se vykoná syntaktická analýza a vybuduje se pracovní tabulka všech identifikátorů, které se v programu používají. Do té si překladač zanese i informaci, že program pracuje s proměnnou ok, typu int, a dále proměnnou s stdin, typu FILE * . Obě proměnné mají extern linkage, což znamená, že budou definované v nějakém externím, zatím blíže neurčeném, modulu programu. Po úvodní analýze překladač vytvoří prototyp strojového kódu programu, který v následných průchodech zoptimalizuje. Výsledek uloží do souboru typu OBJ (Object Module). Soubor multi1.obj má mezinárodně definovaný formát a skládá se ze dvou částí. Jeho základem je relativní strojový kód programu. Ten je neúplný a nelze ho spustit, protože obsahuje řadu odkazů na proměnné typu extern, jejichž adresy se neznají. Místo nich překladač musel použít nějaké neurčené hodnoty ???, např. 0. Na čele OBJ souboru se nachází tabulka obsahující seznam všech extern identifikátorů spolu s odkazy na místa, kde se používají v přeloženém kódu. Kromě nich tabulka obsahuje i identifikátory, které jsou v kódu definované a ostatní moduly je smějí používat. Takové se někdy označují jako export. Soubor multi2.cpp bude přeložen podobně. Napřed nastoupí preprocesor, nahradí "TRUE" řetězcem 1 a makro isdigit odpovídajícími příkazy. Zápis ok=!TRUE; se změnil na ok= ! 1; Výsledek práce preprocesory bude uložený do nějakého dočasného souboru, třeba multi2.tmp, který posléze C++ kompilátor přeloží na multi2.obj. Ten obsahuje relativní kód s proměnnou ok inicializovanou na 0 a dále tabulku s informací, že identifikátor ok udává proměnnou zde definovanou, čili typu export. Po přeložení obou kódů přichází na řadu linker, jehož úkolem je slepit všechny dílčí části dohromady, dopsat chybějící adresy do relativních kódů a vytvořit tak spustitelný program. Při své práci nevystačí jenom s oběma přeloženými OBJ soubory, ale musí připojit i prvky použité $ ze systémových knihoven překladače. Ty mají přípony LIB a ve své podstatě představují pytle souborů typu OBJ doplněné o souhrnné seznamy. Kteroukoliv LIB knihovnu lze zvláštním programem (např. TLIB.EXE u překladače Borland) roztrhnout na soubory OBJ, ze kterých byla složená, a naopak jakékoliv soubory OBJ lze zase spojit do jednoho souboru LIB a tím výrazně urychlit práci programu linker. Linker si vytváří tabulku symbolů, do níž si píše všechny identifikátory typu extern a export a snaží se ke každému extern najít adekvátní export, aby mohl opravit nedefinované adresy. Prohledává všechny OBJ. Pokud se potřebuje nějaký prvek z knihovny, vezme z ní pouze dílčí OBJ, ve kterém se vyskytuje, a to připojí k výslednému programu. Pokud se mu podaří umístit všechny odkazy, zapíše potřebné součásti do dlouhého spustitelného souboru typu EXE. Ten obsahuje výsledný kód programu a pomocné informace pro ope% rační systém. V případě, když linker pro nějaký extern identifikátor nenajde jeho odpovídající umístění nebo naopak odhalí dva totožné export identifikátory, takže se nemůže rozhodnout, na který z nich směrovat odkazy, zastaví svou práci a nahlásí chybu. Při překladu hlásí chyby různé programy: • preprocesor - oznamuje uvedení špatné direktivy preprocesoru, například neexistující include soubor • překladač - vypisuje syntaktické chyby a varování při detekci některých sémantických chyb, např. při použití operace if(i=1) místo if(i==1) • linker - oznamuje nenalezení odpovídají odkazu pro použitý extern identifikátor nebo duplicity export identifikátorů V případě napsání chybné direktivy #define se chyba projeví až při překladu, avšak bude se odkazovat na originální zdrojový text, protože výsledek činnosti preprocesoru má charakter dočasného souboru, který po skončení překladu již neexistuje. Tato skutečnost je vážným
$
Knihovny mohou být dvojího typu statické a dynamické. Zde se pro jednoduchost uvažují pouze statické knihovny. O dynamických knihovnách se bude hovořit v samostatné kapitole. % O struktuře EXE souborů budeme hovořit v kapitole 4.
21
argumentem proti nadměrnému používání maker. V dnešních programech uplatňují většinou jen pro deklarace konstant, jinde se nahrazují inline funkcemi. Opačná situace nastává při chybě linkeru. Pokud se nenajde pro nějaký extern odkaz umístění, linker dokáže vypsat pouze název identifikátoru a modul, ve kterém se vyskytuje, ale není již schopný lokalizovat, jaké části zdrojového textu leží chyba, protože s ním nepracuje. To musí učinit programátor. Lze tedy učinit závěr, že ve všem, co pracuje vně překladače, se špatně hledají chyby. Interprety příkazů zakazující makra, třeba jazyky Java nebo Perl, mají o hodně zjednodušenou práci. Jenže, když ono s těmi makry se občas dá tak skvěle kouzlit... 1.2d.2 Funkce v souborech Multi1 a Multi2 Jazyk C++ rozlišuje dva případy funkcí, normální funkce a funkce typu inline, které se vkládají přímo do kódu programu. Pro každou skupinu platí odlišná pravidla. Pokud chceme, aby nějakou funkci typu inline směly používat i jiné moduly programu, musíme vložit celý její kód do části header, protože inline funkce se bude překládat vložením svých příkazů a kvůli tomu je nutné překladači poskytnout o ní úplnou informaci. Funkce, která není inline, se deklaruje v header souboru tak, že se pouze uvede její hlavička zakončená středníkem, tzv. prototyp funkce. Překladač si uloží identifikátor funkce a popis příslušných formálních parametrů do své tabulky, což mu poskytne dostatek informace k tomu, aby věděl, jakým způsobem má překládat odkazy na tuto funkci. (Její adresu k tomu nepotřebuje znát, tu v případě potřeby zapíše až program linker podle skutečného umístění funkce v paměti.) V části CPP se pak zopakuje hlavička funkce následovaná jejím tělem, tj. jejími příkazy uzavřenými v příkazového bloku { }. Teprve zde vznikne úplný kód funkce. Pokud má funkce některý parametr inicializovaný, inicializace parametru se provede pouze v části header. Zde musí bezpodmínečně být, aby překladač věděl, jakou hodnotou má chybějící parametr nahradit. V hlavičce funkce umístěné v části kódu se pak inicializace již neopakuje, aby se zabránilo duplicitě. U inicializace platí pro parametry funkcí přesně opačné pravidlo než pro proměnné. // Header - soubor *.H inline void CtiRadku(void) { fgets(radka.buf,MAXBUF,stdin); radka.pozice=0; } int CtiCislo(int max, int min=1);
// inicializace min na výchozí hodnotu 1
// Soubor *.CPP int CtiCislo(int max, int min) { char c; //....příkazy funkce. }
// inicializace min vynechaná
1.2d.3 Struktury Struktury, (ale i třídy a uniony), se mohou vytvářet ve třech krocích. Vezměme si RADKA z našeho příkladu: struct RADKA // přidělení tagu { // deklarace členů struktury char pozice; char buf[MAXBUF]; } radka; // definice proměnné radka typu RADKA Zde se provádějí hned tři kroky naráz. Deklaruje se tag, čili informace o tom, že identifikátor RADKA značí strukturu a spadá tedy do name space struktur a tříd. Poté se deklarují členové 22
struktury a na závěr se definuje proměnná radka. Jednotlivé kroky lze provádět i postupně a mezi nimi se může nacházet libovolné množství instrukcí, pokud se dodrží podmínka, že příkazy programu odkazují pouze na již známé prvky. struct RADKA; // Krok 1.: pouhé přidělení tagu, překladač ví, že RADKA je struktura RADKA * pradka; // pointer se dá definovat - odkazuje na strukturu jako na celek. void Nuluj(RADKA * pr) { pr->pozice=0; // CHYBA: Nelze odkazovat na členy struktury; nebyly deklarované. pr ++; // CHYBA: Není známá velikost objektu, na který ukazuje pointer. } struct RADKA // Krok 2.: tag struktury a deklarace členů (rozšíření deklarace kroku 1) { char pozice; char buf[256]; }; // Všimněte si naprosto nutného středníku za závorkou! void Nuluj2(RADKA * pr) { pr->pozice=0; pr ++; // Nyní je vše v pořádku, členy struktury už známe. } RADKA radka; /* Krok 3.: definice objektu radka typu RADKA. Ta je přípustná až teď, protože teprve nyní víme členy struktury a tím i potřebnou velikost paměti, která se musí přidělit objektu radka. */ Do části header můžeme umístit jenom deklaraci hlavičky, tj. krok 1 v případě, že z vnějších modulů odkazuje na strukturu pouze jako na celek, třeba pomocí pointru. Pokud ale potřebujeme odkazovat na členy struktury, musí se tam umístit příkaz označený krok 2 (ten provádí i krok 1). Samotná definice proměnné radka se poté rozloží na deklaraci, ta je v části header a před ní leží klíčové slovo extern. Definice, eventuálně doplněná inicializací, bude pak v programovém souboru (*.CPP). extern RADKA radka; // deklarace radka v header souboru RADKA radka; // CPP soubor. Eventuálně jako: struct RADKA radka; 1.2d.4 Linkage Vlastnost linkage mají výhradně globální objekty, které existují v době běhu programu. Podobně jako duration se nevztahuje na lokální proměnné a objekty existující pouze během překladu, např. na deklarace enum. Kromě datových objektů má dále význam i pro funkce. Linkage nabývá dvou hodnot extern a static. Implicitní hodnota linkage je extern, a proto na jakýkoliv globální objekt můžeme odkazovat z jiného modulu. Stačí pouze vytvořit odpovídají header s informací nutnou pro přeložení příslušného odkazu tak, jak se to rozebíralo v předchozích odstavcích. Linkage se změní na static použitím klíčového slova static před definicí proměnné či funkce. Tím se příslušný identifikátor stane neviditelný pro linker. Neobjeví se v tabulkách popisujících obsah modulu OBJ, a proto na něj nelze odkazovat z vnějších modulů. Změnil se tak v privátní prvek dílčího souboru. (Nezapomínejte rozlišovat static duration a static linkage! Jedná se o dva zcela odlišné pojmy, které mají společné pouze použití klíčového slova static). V našem programu toho využijeme u funkcí CtiZnak z multi2 a CtiZnak1 z multi1. Ty musejí používat jiné identifikátory, přestože se volají výhradně ze svého souboru, aby se při linkování vyloučila duplicita v exportovaných symbolech. Pokud se alespoň u jedné funkce, třeba u CtiZnak v multi2, použije klíčové slovo static a tím se potlačí export jejího symbolu do OBJ, pak lze CtiZnak1 v multi1 přejmenovat na CtiZnak: static char CtiZnak(void) // CtiZnak má static linkage - lze ji volat jen z multi2 a odjinud ne { CtiRadku(); return radka.buf[0]; }
23
Nyní lze rozepsat příkladu rozložený do dvou modulů, každý složený z části header a program. Soubory header obsahují standardní prevenci opakovaného vkládání. Ta začíná dvojicí direktiv preprocesoru, například v multi1.h je to: #ifndef _MULTI1_H_ // nepřekládej, je-li definovaný symbol _MULTI1_H_ #define _MULTI1_H_ // definuj symbol preprocesoru kde #ifndef specifikuje vynechání všech řádků ze souboru až po #endif v případě, že není definovaný symbol preprocesoru _MULTI1_H_. Pokud tento symbol ještě neexistuje, řádky se vloží a vykonají se na nich i příkazy preprocesoru, čili se definuje i _MULTI1_H_. Bude-li poté ještě někde jinde zopakovaný příkaz #include "multi1.h" , pak ho preprocesor sice provede, ale vynechá v něm celý blok #ifndef a nezopakují se proto tak deklarace v něm obsažené. /************* Multi1.h ****************/ #ifndef _MULTI1_H_ // vlož text, není-li definován symbol _MULTI1_H_ #define _MULTI1_H_ // definuj symbol preprocesoru #include <stdio.h> // vložení deklarací pro stdin, fgets(), printf() #define MAXBUF 256 // maximální velikost řádky #define TRUE 1 // konstanta struct RADKA // definice prototypu struktury { char pozice; // aktuální pozice čteni v buf char buf[MAXBUF]; // paměť znaků řádky }; extern RADKA radka; // proměnná definovaná v nějakém externím modulu inline void CtiRadku(void) { fgets(radka.buf,MAXBUF,stdin); // čtení určeného znaků z klávesnice radka.pozice=0; // nulování pozice čteni } #endif // konec podmíněného bloku #ifndef _MULTI1_H_ /************* Multi1.cpp ****************/ #include "multi1.h" #include "multi2.h" static char CtiZnak(void) // dříve CtiZnak1, static = nelze použít odjinud { CtiRadku(); return radka.buf[0]; } int den,mesic,rok; // lze použít odjinud RADKA radka; // vytvoření proměnné definované v multi1.h jako extern void main(void) { do // ošetření výskytu chyby { ok=TRUE; // nastavení příznaku pokračování while(ok==TRUE) // je-li vše v pořádku, převáděj datum { printf("Zadej datum ve tvaru DD.MM.RR (RR=90..99) : "); CtiRadku(); // Funkce z multi2: čteni řádky z klávesnice den = CtiCislo(31); mesic = CtiCislo(12); // funkce z multi2: CtiCislo rok = CtiCislo(99,90); if(ok==TRUE) printf("Datum: %2d.%2d.%2d\n", den, mesic, rok ); else printf("Text: %s\n", radka.buf); // detekována chyba } printf("Chyba - konec programu (a/n) ? "); } while( CtiZnak()=='n' ); // dříve CtiZnak1 - pokračuj, není-li požadavek ukončit práci }
24
/************* Multi2.h ****************/ #ifndef _MULTI2_H_ #define _MULTI2_H_ extern int ok; int CtiCislo(int max, int min=1); #endif /************* Multi2.cpp ****************/
// vlož text, není-li definováno jméno // definuj jméno // inicializace pouze při vytváření proměnné // prototyp funkce - inicializace parametru zde // konec podmíněného bloku #ifndef _MULTI2_H_
#include // isdigit(c) #include "multi1.h" #include "multi2.h" int ok=0; static char CtiZnak(void) // pouze multi2, nelze použít odjinud { static char c; // Pozor, zde má static jiný význam (tady zbytečný) c=radka.buf[radka.pozice]; // čtení znaku, který je na řadě if(c>0) radka.pozice++; // není-li konec řádky, posun pozice return c; }; int CtiCislo(int max, int min) // inicializace parametru min pouze v header { char c; int cislo=0; while( ' '==(c=CtiZnak()) ); // vynecháme mezery if(isdigit(c)) // ? znak je dekadická číslice { cislo = c - '0'; // převedeme ASCII znak na číslo c=CtiZnak(); // čteme další znak if(isdigit(c)) // je-li číslicí, přidáme ho k číslo { cislo = cislo*10 + c-'0'; c=CtiZnak(); // vynechání oddělovače if( isdigit(c) ) ok = !TRUE; //chyba, pokud má číslo víc jak dvě číslice } } if( cislo<min || cislo>max ) ok=!TRUE; // chyba, je-li číslo mimo povolený rozsah return cislo; } 1.2d.5 Name Mangling Existuje několik způsobů jak zavolat funkci a předat jí parametry. To definují klíčová slova: _cdecl - implicitní způsob jazyka C. Parametry předávané funkci se vyčíslují od posledního k prvnímu, tj. zprava doleva, a v identifikátorech se rozlišují velká a malá písmena. Po skončení funkce musí volající program zrušit všechny parametry, které funkci předal přes zásobník (stack). Způsob dovoluje vytvářet funkce, které mají proměnný, předem neurčený počet parametrů, jako například printf. _pascal - styl volání v systému Windows a jazyce Pascal. Parametry se ve funkci vyčíslují od prvního k poslednímu, tj. zleva doprava, a předávají se všechny. V identifikátorech se nerozlišují velká a malá písmena. Volaná funkce při svém skončení sama ruší předaná data na zásobníku. Styl _pascal nabízí menší flexibilitu, ale zato vede v průměru na úspornější kód než _cdecl. _fastcall - rychlé volání funkce s předáváním parametrů v registrech. _stdcall - podobá se _cdecl s výjimkou, že se vyžaduje předávat přesný počet argumentů, protože funkce ruší předaná data na zásobníku.
25
Každý způsob má svoje výhody a nevýhody. Při běžné práci se nemusíme starat metodu volání. Překladač vše zařídí za nás na základě informací v header souborech. Pro zlepšení kontroly chyb provede navíc kvůli rozlišení jednotlivých způsobů volání name mangling identifikátorů funkcí, což znamená transformaci jejich názvů na nový tvar. Například, před identifikátory všech funkcí volaných stylem _cdecl se přidá znak _ (podtržení). Kromě toho překladače C++ přidají k identifikátorům další znaky nesoucí informaci o typu parametrů. Důvodem pro name mangling je zvýšení bezpečnosti kódu a možnost provádění přídavných kontrol při procesu linkování. Linker tak spojuje dohromady nejen podle názvu funkce, ale podle typu jejího volání a použitých parametrů. Tím se vylučuje možnost chyby, jako třeba záměna volaní funkce typu _pascal voláním stylu _cdecl, protože odlišný způsob volání funkce má za následek uložení jiného externího odkazu v OBJ modulu. Existují však situace, kdy je name mangling nežádoucí. Například v případě, že připojujeme nějakou LIB knihovnu pro ovládání průmyslových regulátorů, např. knihovnu PRODAVE pro automaty řady S7. Nemáme od ní zdrojový text, ten nám výrobce pochopitelně nedodal, disponuje jen knihovnou a deklaracemi v souboru header. Ty si náš překladač načte, ale samozřejmě nemusí používat stejný name mangling jako kompilátor, na kterém byla knihovna vytvořená, protože transformace jmen jsou interní záležitostí překladu. Jak takovou situaci řešit? Nelze přece distribuovat verzi knihovny pro každý překladač na světě! Pomocné knihovny proto bývají často přeložené bez name mangling. To lze potlačit pomocí klíčového slova extern "C". Zápis: extern "C" void ResetPLC(void); potlačí name mangling pro identifikátor ResetPLC. Pokud si v header souboru přejeme zabránit name mangling pro víc identifikátorů, můžeme všechny deklarace uzavřít do příkazového bloku: extern "C" { void fce1(int i); void fce2(long l); void fce3(double d); }; Respektivě lze totéž udělat přímo v kódu CPP u příkazu #include (ten přece znamená vložení řádek souboru): extern "C" { #include "LibPLC.h" }; Pokud se marně pokoušíte připojit ke svému programu nějakou knihovnu a linker vám hlásí, že její funkce nemůže najít, přestože jste si jistí, že v ní určitě existují, zkuste potlačit name mangling pro její header soubor. Naopak, budete-li sami distribuovat nějakou LIB knihovnu funkcí, přeložte ji bez name mangling. S tím se setkáme ještě v kapitole 9.2 věnované dynamických knihovnách Windows.
1.3 Pointry a adresace procesoru Pointry se obvykle vysvětlují jako abstraktní typy, jako jakési pomyslné odkazy na proměnné, jejichž struktura se považuje za interní záležitost překladače, a nesluší se, aby se jí programátor zabýval. Pouze padne letmá zmínka o tom, že pointry mají význam adres procesoru. Skripta zkusí tradici porušit a pointry přiblížit přesně opačným postupem - od způsobu adresace procesorů INTEL k vlastnostem pointrů jazyka C a významu operací s nimi. To současně umožní pochopit správu virtuální paměti ve Win32 a rozdíly mezi 32 a 16-ti bitovými systémy, čehož bude využito v pozdějších kapitolách. Pointry jazyka C mají velmi efektivní překlad a dovolují plně využívat možností procesoru. Představují však často diskutovaný prvek. Jazyky Java nebo Perl pointry vůbec neznají, Pascal 26
s nimi pracuje, ale klade na ně četná omezení, která snižují pravděpodobnost chyby v kódu. Jazyk C zachází s pointry zcela volně a bezpečnost programu ponechává na zkušenosti programátora a dokonalosti operačního systému. Na tento rizikový faktor upozorňují kritici jazyka C a staví se k pointrům skoro tak jako k instrukcím goto. Když však sestoupíme na úroveň procesoru, zjistíme, že jeho strojový kód se opírá především o adresy, čili prvek blízký pointrům, a o instrukce skoků, ekvivalenty goto. Jakýkoliv program, který napíšeme, bude nakonec provedený jako rej skoků a pointrů, neboť tak dnešní procesory pracují. Pokud odmítneme pointry jako nezdravý prvek v moderním programování, vytváříme umělou bariéru. Procesor bude naše příkazy vykonávat pomocí pointrů, ale my je používat nesmíme. V jazyce Java, určeném pro práce na síti, má tato bariéra svá opodstatnění. Nemůžeme přece poskytnout cizímu programu, který se k nám dostane po drátech neznámo odkud, nekontrolovatelný přístup k našemu počítači. Ze stejného důvodu se Java apletùm dokonce brání pracovat se soubory. Smějí pouze kreslit na obrazovku, hrát zvuky a podobně. V jazyce C by podobná omezení nepochybně markantně zvýšila bezpečnost, ale na druhou stranu by znamenala zatraceně závažnou překážku pro jakoukoliv práci, a proto se o spolehlivost programu raději postaráme sami správným používáním pointrů.
1.3a
Adresový prostor procesoru
Pointry mají svoje analogie ve způsobu jak procesor vytváří adresy proměnných. Dnes převažují 32-bitové programy, ale strukturu jazyka C silně poznamenala 16-ti bitová prostředí MSDOSu a Win16, a proto přehled adresování začneme od nejstaršího procesoru 8086. Výklad adresace procesorů INTEL se opírá o následující pojmy: fyzická adresa - nebo také reálná adresa - představuje vnější paměť procesoru. Tu u osobních počítačů PC tvoří dlouhý řetězec bytů, kterým jsou přiřazené číselné adresy od nuly do maximální dostupné fyzické adresy pro příslušný procesor. Fyzická adresa představuje ve své podstatě pořadové číslo bytu. To procesor vysílá na adresovou sběrnici, když žádá o přečtení, respektivě o uložení dat. 16-ti bitové procesory čtou a zapisují dva byty najednou a na adresový sběrnici vysílají sudou adresu A a pomocí ní na datové sběrnici pracují s dvěma byty fyzické paměti A a A+1. 32-bitové procesory dostupují na čtyři byty najednou. Posílají adresu A dělitelnou čtyřmi a na datové sběrnici operují se čtyřmi byty fyzické paměti A, A+1, A+2 a A+3. Fyzický adresový prostor nemusí být nutně plně obsazený. Například procesory Pentium mají fyzický adresový prostor 4 GB, z něhož většina počítačů využívá necelou setinu. virtuální adresa - nebo také logická adresa - znamená způsob adresace dat v instrukčním kódu procesoru. Virtuální adresový prostor, neboli virtuální paměť, představuje oblast všech možných adresací, které lze definovat v kódu procesoru. Procesor převádí virtuální adresy na fyzické adresy nějakým vhodným zobrazením, který většinou nebývá jednoznačné. Ke každé virtuální adrese nemusí nutně existovat fyzická adresa a naopak několik virtuálních adres se může převádět na jednu fyzickou adresu. Například virtuální adresový prostor procesoru Pentium má velikost 64 TB, zatímco fyzický jen 4 GB, takže v daném okamžiku mohou existovat odpovídající fyzické adresy. pouze ke zlomku virtuálních adres. Naproti tomu, u procesoru 8086 existuje fyzická adresa k většině virtuálních adres, ale na druhou stranu až 4096 různých virtuálních adres se může převádět na jednu fyzickou adresu. lineární adresa - virtuální adresa se nemusí vždy mapovat přímo na fyzickou adresu. Počínaje procesorem 80386 se virtuální adresa přepočítává ve dvou krocích. Procesor ji napřed konvertuje na číselnou hodnotu, tzv. lineární adresu, která svým rozsahem odpovídá intervalu všech fyzických adres. Lineární adresa se znovu mapuje na odpovídající fyzickou adresu. Postup mapování adres můžeme vyjádřit obrázkem Obr. 1-2 pro případ, že fyzický adresový prostor zahrnuje interval < 0, MAXFYZ >. 27
V i r t u á l n í
a d r e s a
L i n e á r n í
a d r e s a
mnohoznačné zobrazení
jednoznačné zobrazení
0
MAXFYZ
F y z i c k á
a d r e s a
0
MAXFYZ Adresní sběrnice procesoru
Obr. 1-2 Mapování virtuální adresy na fyzickou adresu
1.3b
Procesor 8086 a MS-DOS
Operační systém MS-DOS se opíral o architekturu procesoru 8086, která obsahovala několik velmi nešťastně zvolených prvků • použité mapování virtuálních adres na fyzické limitovalo fyzický adresový prostor na 1 MB; • procesor nezačal po spuštění vykonávat instrukce od adresy 0, ale od konce svého fyzického adresového prostoru (od fyzické adresy 0xFFFF0). To mělo za následek, že výrobci počítačů museli umístit paměť ROM s inicializačním programem počítače do horní části adresového prostoru, čímž se zablokovala možnost spojitě rozšiřovat paměť směrem nahoru. Virtuální adresový prostor určovaly čtyři 16-ti bitové registry (CS, DS, ES, SS) zvané segmentové registry (segment registers) a 16-ti bitové všeobecně použitelné registry, kterých bylo osm, ale jen šest z nich (BX, DX, SI, DI, BP, SP) se smělo podílet na vytváření virtuální adresy. Virtuální adresa měla dvě složky - 16-ti bitový segment a 16-ti bitový offset a zapisovala se ve tvaru segment:offset. Fyzická adresa se z ní počítala podle vzorce: fyzická adresa = segment * 16 + offset; Fyzická adresa existovala téměř ke všem virtuálním adresám. Výjimku tvořilo několik adres, u nichž přepočet překračoval povolený horní limit fyzické adresy 1 MB a výsledek převodu byl nedefinovaný jako např. u adresy 0xFFFF:0xFFFF. Opačný převod z fyzické adresy na virtuální nebyl jednoznačný. Fyzická adresa se ve většině případů dala vyjádřit mnoha způsoby, pro některé adresy až 4096. Například virtuální adresy 0:0x1000, 1:0xFF0, 2:0xFE0, 3:0xFD0 ... 0x100:0, se převádějí na stejnou fyzickou adresu 0x1000. Pro většinu instrukcí existoval předdefinovaný segment registr použitý ve virtuální adrese, a proto stačilo v kódu uvádět pouze hodnotu offsetu. Například instrukce MOV (operace přiřazení) používala jako výchozí segment registr DS a zápis: MOV AX,[SI] znamenal požadavek nahrát do akumulátoru AX obsah virtuální adresy DS:SI. Pokud se měl použít jiný segment registr, muselo se to explicitně definovat, jako třeba: MOV AX,ES:[SI] specifikuje virtuální adresu ES:SI. Použití jiného segment registru než přednastaveného prodloužilo kód instrukce o jeden byte, o tzv. instrukční prefix.
28
Všeobecné registry
Řídicí registry Instruction Pointer
IP
AX
Accumulator
Flags
AF
CX
Counter
BX
Base
DX
Data
SI
Source Index
DI
Destination Index
BP
Base Pointer
SP
Stack Pointer
Segmentové registry Code Segment
CS
Data Segment
DS
Extra Segment
ES
Stack Segment
SS
Převod virtuální adresy 16-ti bitový segment
15..12
+ 19..16
11..8
7..4
3..0
15..12
11..8
7..4
3..0
16-ti bitový offset
15..12
11..8
7..4
3..0
20-ti bitová fyzická adresa
Obr. 1-3 Princip adresace u procesoru 8086 Instrukční soubor dovoloval použít i číselnou hodnotu v adrese a připouštěl rozličné kombinace registrů, z nichž se počítal offset. Například: MOV AX,CS:[0x1000 + BX + SI] udává naplnění AX obsahem virtuální adresy s offsetem tvořeným součtem hexadecimálního čísla 0x1000 a dvou registrů BX a SI. Pouze dvě instrukce, skoku JMP a volání podprogramu CALL, mohly mít za operand adresu obsahující absolutní segment i offset, jako třeba příkaz volání podprogramu z adrese 0x1000:0 CALL 0x1000:0 Instrukce CALL dovolovala i adresu bez udání segmentu, například CALL 0x1000 která měla význam volání podprogramu na adrese určené CS:0x1000+IP, tj. volání podprogramu v segmentu zadaném obsahem CS registru a pozicí relativní vůči obsahu IP, čili adrese právě vykonávané instrukce. S výjimkou CALL a JMP musely ostatní instrukce používat segment uložený v některém segment registru a k němu doplňovaly pouze 16-ti bitový offset. Segment registry sloužily pouze pro uložení hodnoty segmentu a nedaly se s nimi provádět aritmetické operace. Snadno se měnila jen hodnota offsetu, avšak pomocí té se dalo pracovat s bloky o maximální délce 64 kB. Praktickým důsledkem této adresový architektury, která dodnes ovlivňuje jazyk C, jsou dva typy adres proměnných: • near adresa - udaná pouze offsetem, u níž se předpokládá použití nějakého předdefinovaného segmentu, • far adresa - specifikovaná segmentem a offsetem,
29
Stejně tak se vyskytovaly dva druhy volání podprogramů: • near - volání podprogramu uvnitř segmentu CS, při němž se na zásobník se ukládala jako návratová adresa pouze hodnota IP = offset následující instrukce, • far - volání podprogramu, která předávalo řízení do jiného segmentu, a proto se na zásobník ukládaly jako návratová adresa původní obsahy registrů IP a CS, Poznámka: Existuje ještě třetí způsob volání podprogramu - během přerušení - při němž se na zásobník ukládají tři registry AF, IP a CS.
1.3c
Procesor 80286 a Win16
Procesor 80286 přinesl velmi problematická zlepšení zaměřená na odolnost počítače proti narušení, avšak problémy s adresací řešil jen v omezené míře. Dříve než ho programátoři začali používat, byl vyřazený z výroby. Dokonce ani Windows řady 3.x, které se opíraly o jeho architekturu, ho nikdy plně nevyužívaly. Když se objevil jeho nástupce, procesor 80386, byly rozšířené o nové možnosti, avšak jejich jádru zůstala omezení z původního procesoru 286, týkající se zejména maxima 16 MB paměti, kterou mohl uživatelský program využívat Procesor 80286 měl dva módy práce • reálný mód - který byl kompatibilní s procesorem 8086; • chráněný mód (protected mode). Do chráněného módu se procesor přepínal zvláštní instrukcí a po jejím vykonání změnil mapování virtuálních adres na fyzické na zcela odlišný způsob opírající se o tabulky uložené ve fyzické paměti počítače. Zpět do reálného módu se vracel pouze vnějšího signálu RESET, což PC počítače s 286 komplikovaně prováděly přes port klávesnice - programy totiž musely zapínat a vypínat chráněný mód procesoru při každému dostupu do paměti nad 1 MB. Práce v chráněném módu je extrémně složitá a její vysvětlení leží mimo rozsah této publikace. Osobně nedoporučuji, aby se o ni čtenáři zajímali hlouběji. Při psaní běžných programů vystačí pouze se znalostí mapování virtuálních adres, která bude uvedena dále. 16-ti bitový offset
16-ti bitový selektor
Převod virtuální adresy 15
3
13-ti bitový index
2
1..0
TI
RPL
Tabulka popisovačů segmentů 8192 položek (64 bitových)
GDT/LDT
24 bitová báze segmentu
... ...
63
24 bitová fyzická adresa
0
Obr. 1-4 Princip mapování virtuálních adres u procesoru 80286
30
Procesor 80286 používal totožnou strukturu registrů jako procesor 8086 (viz. Obr. 1-3) doplněnou o speciální registry pro uložení parametrů chráněného módu. Nejvýznamnějšími novými registry byly 40-ti bitový GDTR a 56-bitový LDTR (Global / Local Descriptor Table Register = globální / lokální tabulka popisovačů segmentu) určující umístění tabulek pro mapování virtuálních adres. Každá tabulka, GDT a LDT, obsahovala 8192 položek o délce 64 bitů, v nichž byla uložená 24 bitová bázová (počáteční) adresa segmentu a dále informace o délce segmentu a přístupových právech. V chráněném módu měly adresy opět 16-ti bitový offset, ke kterému se odpovídají segment definoval způsobem shodným jako u procesoru 8086, ale podstatně se změnil se význam hodnotu segmentu, a proto se změnilo i jeho označení - místo o registrech segmentů se hovoří o registrech selektorů segmentu. Horních 13 bitů registru selektoru udávalo index do jedné z tabulek popisovačů segmentů, další bit (TI = Table Indicator) vybíral příslušnou tabulku GTD či LDT a poslední dva bity specifikovaly požadovanou úroveň oprávnění při přístupu na adresu (RPL - Requested Privilege Level). RPL měla čtyři hodnoty, 0 nejvyšší, 3 nejnižší. Běžné programy pracují téměř vždy na úrovni 3, úroveň 0 si rezervuje OS a zbylé dvě úrovně (1 a 2) se zpravidla nepoužívají. Pokud program žádá data nebo volá podprogramy, které leží v segmentech se stejnou nebo nižší úroveň oprávnění, je vše v pořádku. Opačná situace, tj. přístup na vyšší úroveň oprávnění, což občas bývá nezbytné, patří k velmi složitým otázkám činnosti procesoru a v tomto skriptu se jí nebudeme zabývat.
1.3d
Procesory 80386 až Pentium a Win32
Procesor 80286 zavedl komplikovaný chráněný mód, ale vůbec neřešil problémy s adresací. Jeho fyzický adresový prostor zaujímal pouze 16 MB paměti a zůstal nepříjemný limit jednoho segmentu na 64 kB. Podstatné zlepšení přinesl až procesor 80386, který konečně nabídl vyhovující způsob adresace. Ten od něho přebírají všichni jeho následovníci. Přestože Win16 byly pro nový procesor upravené, jeho možnosti plně využily až Win32. Největší změnou v procesoru 80386 bylo prodloužení všeobecných registrů na 32 bitů. 32 bitové registry se označují prefixem E, např. EAX značí 32 bitový akumulátor, jehož dolních 16 bitů tvoří 16-ti bitový akumulátor AX. Dále došlo k přidání třetího módu činnosti. Procesor 386 měl: • reálný mód kompatibilní s procesorem 8086; • chráněný mód (protected mode) shora kompatibilní s procesorem 80286, avšak doplněný o mapování virtuální adresy ve dvou krocích, tj. podle Obr. 1-2; • virtuální mód 8086, dovolující z chráněném módu spustit proces s adresací procesoru 8086 bez nutnosti ukončit chráněný mód, tj. MS-DOS může běžet jako jeden z procesů. První krok mapování, tj. převod virtuální adresy na lineární, probíhal takřka shodně s procesorem 80286 až na následující rozdíly: • offset měl délku 32 bitů; • tabulky GDT a LDT obsahovaly 32 bitovou bázovou adresu segmentu; • procesor 386 měl o navíc dva další registry selektorů označené FS a GS; • při vytváření adresy se mohly používat všechny všeobecné registry; Druhý krok mapování dovoloval elegantní správu paměti. Lineární adresa byla rozdělena na 4 kilobytové bloky zvané stránky. Každé stránka měla 32 bitový specifikátor udávající přístupová práva na ni a její okamžité umístění ve fyzické paměti, tj. pořadové číslo 4 kB bloku fyzické paměti, na který se zobrazí. Specifikátory stránek popisovala matice uložená po řádcích a adresy začátků jednotlivých řádek udávala tabulka.
31
16-ti bitové registry selektorů
32 bitové všeobecné registry EAX
EBP
ES
CS
EBX
ESI
FS
DS
ECX
EDI
GS
SS
EDX
ESP
32 bitový offset
16-ti bitový selektor
Převod virtuální adresy na lineární 15
3
13-ti bitový index
2
1..0
TI
RPL
Tabulka popisovačů segmentů 8192 položek (64 bitových)
GDT/LDT
32 bitová báze segmentu
... ...
63
32 bitová lineární adresa
0
Obr. 1-5 Princip mapování virtuální adresy na lineární u procesoru 386 Při mapování se lineární adresa se rozdělila na tři části. Horních 10 bitů specifikovalo index řádku matice a dalších deset bitů určovalo index sloupce matice. Dolních 12 bitů popisovalo pozici uvnitř stránky a postupovalo přímo na výstup, kde se k nim připojila 20-ti bitová adresa, tzv. adresa rámce, která se přečetla z matice specifikátorů stránek. Celý proces přes svoji složitost probíhal velmi rychle. Procesor ho urychloval pomocí interních vyrovnávacích pamětí a plně asociativní paměti zvané TLB (Translation Look-aside Buffer), takže se minimálně přistupovalo do transformačních tabulek uložených ve fyzické paměti. Úplná matice specifikátorů stránek by měla rozměr 1024 * 1024 prvků a zabrala by celkem 32 MB paměti, a proto většina adres řádek zpravidla směřuje do prázdna a má nastavený příznak neplatné adresy. Při pokusu o dostup na ně se hlásí chyba výpadku stránku a operační systém má možnost uvolnit fyzickou paměť, například uložením části paměti přidělené jinému procesu na disk, a opět vrátit řízení původnímu programu. Win32 dovolují tento mechanismus využívat formou výjimek, o nichž se bude hovořit v kapitolách 8.5 a 8.6.
32
32-bitová lineární adresa
22 21
31 index řádky
10 bitů
12
index sloupce
11
0
12-ti bitový offset
10 bitů
...
20 bitů
... ...
...
12 bitů
32 bitů
X X
Mat ice 32-bi tový ch speci f ikáto rů 4 k B st rá n e k f yz ic k é p a m ě t i
X Tabulka adres řádků matice (1024 položek)
32 bitová fyzická adresa
Obr. 1-6 Princip převodu lineární adresy na fyzickou adresu
1.3e
Modely rozložení paměti
Práce s pamětí, a tedy i chování pointrů, závisí na prostředí, ve kterém programujeme. Naštěstí již končí doba aplikací pro 16-ti bitová prostředí, která si žádala velmi komplikovanou správu paměti. 32 bitové prostředí situaci zjednodušuje. Základní otázkou při správě paměti je rozmístění částí programu: • kód, čili vykonávané instrukce. Ty se pouze čtou a do této paměti není dovolený zápis. • globální data - ty se vytvářejí v paměti během spouštění programu a zůstávají v ní až do jeho konce. Tato paměť zaujímá konstantní velikost. • zásobník (stack) - kde se ukládají návratové adresy a parametry podprogramů a lokální proměnné, tato paměť se za běhu programu rozšiřuje směrem k nižším adresám. • heap - volná paměť, z níž program může podle potřeby čerpat místo pro dynamické proměnné, např. operátory new a delete. Existuje celkem šest vhodných modelů pro prostředí MS-DOS, z nichž tři se používají i v prostředí Win16. Win32 provádějí zcela odlišnou správu paměti, proto vystačí s jediným modelem. Všimneme si pouze architektury modelu large (rozlehlý), v němž pracuje většina aplikací vytvořených pro Win16, a potom zaměříme se na model flat (plochý) pro Win32.
33
N segmentů každý max. 64 kB
Model large CS1 → CS2 → CS3 →
Kód programu segment 1
Model flat DS, ES, SS, CS →
Kód programu volně
Kód programu segment 2
rozmístěný v adresovém
Kód programu segment 3
prostoru 4 GB
CSN → DS →
Globální data max. 64 kB SS →
Globální data
Zásobník (stack) max. 64kB Okamžitý ESP →
Počáteční SP →
Počáteční ESP →
Heap
Heap
Okamžitý SP →
Adresový prostor 4 GB
Kód programu segment N
Obr. 1-7 Modely správy pamìti programù 1.3e.1 Paměťový model large Model large se hodí pro 16-ti bitové aplikace, které mají velké množství kódu a nepotřebují víc globálních dat než 64 kilobytů, tj. jeden segment. Dva segmentové registry jsou pevně umístěné, DS na začátek segmentu globálních dat a SS na segment zásobníku. Instrukce se nacházejí v několika segmentech a odpovídající CS segment registr se mění podle potřeby. Model large dovoluje přistupovat na globální a lokální data pomocí near adres, tj. pouhého 16bitového offsetu, což zrychluje práci a zkracuje objem kódu. Far adresy (segment:offset) se potřebují pouze pro dynamické objekty umístěné na heapu, který tvoří zbylá volná paměť. Pokud program požádá o paměťový blok nějaké délky, operační systém mu přidělí kus heapu, samozřejmě, je-li v něm volný úsek požadované velikosti. Existuje celá řada strategií pro přidělování paměti na heapu, od velmi komplikovaných až po primitivní. Jedna z jednodušších metod nakládá s heapem jako se zásobníkem. Pokud program požádá o úsek paměti, přidělí mu ho na konci heapu a adekvátně posune ukazatel vrcholu heapu. Tato technika se dá skombinovat s přednostním pátráním po volném místě pod vrcho34
lem heapu, tj. po dříve přidělené, ale již uvolněné části a podobně. Strategiemi přidělování heapu se nebudeme hlouběji zabývat, protože pro jazyk C, s výjimkou speciálních úloh, nemají klíčový význam jako pro jazyky opírající se převážně o dynamické proměnné, např. pro LISP. 1.3e.2 Paměťový model flat V modelu flat pracují dnes všechny 32 bitové aplikace. Ten vychází z architektury mapování virtuálních adres ve dvou krocích, kterou přinesl procesor 386. Operační systém pevně umístí všechny selektory a program s nimi nemanipuluje a k adresování proměnných používá výhradně 32 bitový offset. Přeložený kód a data mají společný virtuální adresový prostor o délce 4 GB. Vzhledem k tomu, že všechny selektory mají pro aplikaci stále konstantní hodnoty, je i fixní výsledek prvního mapování adres, tj. převod z virtuálních na lineární adresy. Adresy používané programem lze proto pokládat za lineární adresy, odtud i název modelu - plochý. Operační systém je mapuje na fyzickou paměť, viz. obrázky Obr. 1-2 a Obr. 1-6. Praktickým důsledkem mapování je skutečnost, že jednotlivé části programu mohou být libovolně rozeseté po přiděleném 4 GB adresovém prostoru a neleží těsně za sebou, jako to bývalo zvykem v MS-DOS programech. V případě části kódu bývá přeložený program umístěný někde na začátku, zatímco systémové služby okupují střed horní poloviny. Konkrétní polohy závisejí na operačním systému a překladači. Pro programátora jsou nejdůležitější data. Polohy jednotlivých částí odpovídají svými pozicemi probranému modelu large s výjimkou, že zde neplatí limity 64 kB. Například pointer zásobníku (32 bitový registr ESP) se zpravidla umísťuje nad globálními daty tak, aby měl 1 MB volného prostoru, což lze nastavení překladače, eventuálně linkeru, změnit. Rovněž se zjednodušuje i práce s heapem, operační systém dovoluje, aby program požádal o konkrétní adresový prostor, dovoluje jeho rezervaci i alokaci při výpadku stránky. Techniky práce se s heapem v modelu flat se budou podrobněji rozebírat v kapitolách 8.5 až 8.7. 1.3e.3 Large versus flat Na závěr této části několik poznámek k 32-bitovým a 16-ti bitovým programů. Obecně neplatí, že 32 bitové programy jsou výkonnější než 16-ti bitové, spíš naopak. V modelu large lze globální i lokální proměnné adresovat pomocí 16-ti bitové near adresy. Vzhledem k tomu, že 32 bitové procesory čtou 4 datové byty najednou, mohou číst spolu s 2 bytovým offsetem ještě další data. Naproti tomu 32 bitové programy používají 4 bytové offsety adres a na jejich načtení potřebují celý cyklus. Přechod na 32 bitové programy znamená proto většinou zpomalení práce procesoru, jehož velikost závisí na konkrétní úloze. Pro běžné aplikace se udává kolem dvaceti až třiceti procent. 32 bitové programy naproti tomu nabízejí lepší práci s velkými bloky dat. V modelu large znamená každý přístup na heap naplnění některého registru selektoru, což komplikuje program. 32 bitové programy pracují s heapem stejně tak dobře jako s kterýmikoliv jinými daty. Na tuto skutečnost se bohužel často hřeší. Zatímco v 16-ti bitových programech se programátoři pečlivě rozmýšleli na každou dynamickou proměnnou a snažili se o rozličné efektivní metody práce s rozlehlými prvky, ve modelu flat se často všechna data, potřebná i nepotřebná, trvale umístí do paměti, ať si s tím operační systém poradí. On si s tím opravdu poradí až na to, že důkladně přesype brambory, jak se v programátorské hantýrce říká rachocení disku, ale to přece nevadí, vždyť to dá svést na nedokonalost Windows. Jiným zlozvykem, zejména začátečníků, který snižuje efektivnost programu, je zbytečné šetření proměnnými. V dávných dobách, kdy se paměti měřily na kilobyty a se úspora proměnné považovala za profesionální hrdost. Dnes k tomu není vážnější důvod, spíš naopak. Zbytečná redukce proměnných, zejména v případě malých lokálních dat, snižuje přehlednost kódu a nezvýší rychlost výpočtu, spíš ji v mnoha případech zpomalí.
35
1.4 Pointry v jazyce C
1.4a
Typy pointrů
Jazyk C zná tři typy pointrů, ale pouze jediný z nich, typ near se používá ve 32 bitových programech. Ostatní mají význam pouze pro MS-DOS a Win16. Near pointry odpovídají near adresám. Mají význam offsetu a předpokládá se pro ně použití nějakého implicitního segment registru podle vhodného paměťového modelu. Zabírají 4 byty paměti pro 32 bitové programy a 2 byty pro 16-ti bitové programy. Far pointry reprezentují far adresu procesoru, tj. dvojici selektor:offset, resp. v MS-DOSu segment:offset. Jsou uložené ve 4 bytech. V 16-ti bitovém prostředí horní 2 byty obsahují selektor a dolní 2 byty mají význam offsetu. & V 32-bitových programech se všechny deklarace far pointrů automaticky nahrazují 4 bytovými near pointry. Far pointry dovolují zadat libovolnou adresu v celém virtuálním adresovém prostoru, avšak jazyk C pro ně definuje operace sčítaní a odčítání, které ovlivňují pouze jejich offsety, tj. hodnota selektoru se operacemi nemění. V 16-ti bitových programech se proto nedají použít pro práci s bloky dat, které svoji délkou přesahují 64 kilobytový segment paměti. Huge pointry mají stejný formát jako far pointry, ale na rozdíl od nich dovolují práci s bloky dat většími než 64 kB. Toho se dosahuje automatickou normalizací huge pointru. Po každé operaci s ním se přepočítávají hodnoty jeho selektoru a offsetu tak, aby offset ležel v nějakém intervalu <0..maxoff> a přitom zůstala zachována cílová fyzická adresa. Pro systém MS-DOS se offset udržuje v rozsahu <0..0xF> a příslušný segment se přepočítává podle vzorce pro výpočet fyzické adresy. Například, nabude-li huge pointer po nějaké aritmetické operaci hodnoty 0x100:0x15, přepočte se na 0x101:5, protože: 0x100 * 0x10 + 0x15 = 0x1015 = 0x101 * 0x10 + 5 Ve Win16 je situace mnohem složitější. Pokud alokujeme velký blok, např. 1 MB dat, pak nám operační systém vrátí adresu jeho začátku ve formě pointru selektor:offset. Předpokládejme, že ten se rovná 0x100F:0. Pomocí něho můžeme adresovat pouze blok dlouhý 64 kB ležící v intervalu adres <0x100F:0, 0x100F:0xFFFF>, a proto Win16 vytvoří i další selektory. Neprovede ale překrytí paměti podle vzorce pro procesor 8086, tj. s krokem 0x10, protože disponuje pouze 8192 možnými selektory a musí s nimi šetřit. Použije kvůli tomu větší krok v offsetu, jehož hodnota patří mezi systémové konstanty a bývá 0x1000. Za selektorem 0x100F bude ' následovat selektor 0x1017 a huge pointer 0x1017:0 bude směřovat na stejnou adresu jako pointer 0x100F:0x1000. Podobně, huge pointer 0x101F:0 bude odpovídat 0x100F:0x2000 atd. Náš 1 MB blok obdrží celkem 256 selektorů ( = 1 mega / 0x1000 ). Huge pointry představují velmi nešikovný způsob jak obejít limit 64 kB. Nutná normalizace po každé operaci zdržuje a navíc se může v řadě případů provést špatně, například, když k pointru omylem připočteme příliš velké číslo a offset nám přeteče. Lepší je proto raději upravit algoritmus tak, aby se huge pointry nemusely používat, nebo přejít do 32-bitového prostředí. Typ pointru se při deklaraci udává klíčovým slovem: int near * npint; // near pointer na proměnnou npint typu int char far * lpchar; // far pointer na proměnnou lpchar typu char long huge * hplong; // huge pointer na proměnnou hplong typu long pokud vynecháme označení typu jako např. double * pKoef; // pointer na proměnnou typu double &
V 16-ti bitových programech lze pomocí předdefinovaných maker v dos.h z far pointeru extrahovat hodnotu selektoru a offsetu (FP_SEG a FP_OFF), a naopak ze selektoru a offsetu zase složit far pointer makrem MK_FP. ' Dolní 3 bity selektoru obsahují výběr tabulky a úroveň přístupu (viz. část o procesoru 286 na straně 30), a proto zůstávají konstantní. Pro uživatelské programy bývají tyto dolní 3 bity nastavené trvale na 1.
36
pak překladač použije implicitní hodnotu v závislosti na použitém paměťovém modelu. Pro model large je implicitní typ pointru far, protože se předpokládá, že bude použit k dostupu na heap, což vyžaduje far pointer. Ve 32 bitovém prostředí jsou všechny pointry typu near bez ohledu na jejich deklaraci. Mají délku 32 bitů a umožňují bez problémů adresovat bloky dlouhé až 4 GB. Použijeme-li klíčové slovo far, překladač ho bude ignorovat, avšak deklarace huge pointrů ohlásí jako chyby. V dalším textu se bude předpokládat, že se program kompiluje pro model large nebo flat, v nichž všechny pointry bez uvedení typu a s typem far zabírají vždy 4 byty paměti. Za tohoto předpokladu, existuje jediný rozdíl mezi chováním pointrů ve 32 bitovém a 16-ti bitovém prostředí (tj. 32 bitového near pointru a 32 bitového far pointru), a to v rozsahu paměti, kterou lze přes ně adresovat. Pro malé bloky dat se oba druhy pointrů chovají totožně a není nutné přihlížet k rozdílům mezi 16-ti a 32 bitovými programy. Případné diference budou vždy patřičně zdůrazněné. Huge pointrům se dále nebude věnovat pozornost.
1.4b
Základní operace s pointry
Uvažujme tři deklarace pointrů: int * pi; char * pc; long * plong; každá má jiný datový typ, ale všechny pointry představují adresy, a proto zaujímají totožnou délku v paměti. Výsledek operace sizeof, tj. dotaz na počet přidělených bytů paměti, bude pro ně rovný 4. Uvedený datový typ mění výhradně chování operátorů, které s pointrem pracují, a nemá vliv na hodnotu adresy uložené v pointru. Vytvořený pointer má, stejně jako každá jiná proměnná v jazyce C, nedefinovaný obsah, jedná-li se o lokální proměnnou, nebo je inicializovaný na 0, jde-li o globální proměnnou. Před prací s pointrem se do něho musí uložit platná adresa, například pomocí operátoru & , který vrací adresu prvního bytu proměnné. long udaj; // vytvoření proměnné long plong = & udaj; // do pointru plong uložíme adresu proměnné udaj Datový typ pointru specifikuje pro operátor * velikost a numerický formát dat, které se budou číst, resp. ukládat, například: * plong = 0x41424344L; // uložení čísla na adresu obsaženou v pointru printf("%lx", * plong ); // výstup 41424344 = číslo long přečtené z adresy printf("%lx", udaj ); // výstup 41424344 = obsah proměnné udaj Uložení čísla prostřednictvím pointru plong, který ukazuje na proměnnou udaj, odpovídá zápisu tohoto čísla do proměnné udaj. Pointer lze použít i ke čtení jiného datového typu než jeho vlastního, třeba typu char, pokud se použije operátor (type) pro přetypování: printf( "%c", * (char *) plong ); // vypíše ´D´ = 0x44 - nejnižší byte čísla údaj V tomto případě jsme použili adresu uloženou v plong a pomocí přetypování jsme upravili k ní přiřazený datový typ na char. Obsah samotného pointru plong se tím nezměnil, stále se rovná adrese, na níž je v paměti umístěná proměnná udaj, avšak přetypování ovlivnilo chování operátoru *. Stejně tak, provedeme-li: * (short int *) plong = 0x6364; // uložení čísla short int prostřednictvím pointru printf("%lx", udaj ); // výstup bude = 41426364, změnily se 2 dolní byty
Z důvodu zpětné kompatibility se pro klíčové slovo far definuje jeho náhrada prázdným řetězcem: #define far Pro zapomětlivé připomínám, že čísla se ukládají od nejnižšího k nejvyššímu bytu, tj. obsah proměnné udaj bude v paměti uložený jako čtveřice bytů | 0x44 | 0x43 | 0x42 | 0x41 |.
37
Výše uvedenou operaci šlo napsat i takto: shot int * pi; // pointer na proměnnou typu short int psi = (short int *) plong; // přiřazení obsahu pointru do pointru odlišného typu * psi = 0x6364; printf("%lx", udaj ); // výstup bude = 0x41426364 Přetypování při přiřazení se muselo provést, protože oba pointry měly jiný datový typ. Kdyby se jednalo o stejné typy, bylo by zbytečné: long * pdata; pdata = plong; // přiřazení obsahu pointru do stejného typu Pointer nemusí nutně mít přiřazený datový typ. Pointry typu void ho nemají, a proto nelze na ně aplikovat žádné operace, které provádějí čtení či zápis. Jejich výhodou je, že se na ně převede jakýkoliv pointer bez nutného přetypování, a proto se často používají jako formální parametry funkcí: void * pvoid; // pointer void bez přiřazeného datového typu pvoid = plong; // do pvoid něho uložit libovolný pointer *pvoid = 0; // Chyba, operace není definována, není znám datový typ * (long *) pvoid = 0; // Zápis 0 ve tvaru long čísla na adresu pvoid char * str = "ALFA"; byty paměti
'A'
char * str;
'L'
'F'
'A'
0
"ALFA" Obr. 1-8 Pointer inicializovaný řetězcem znaků Pointer slouží často pro práci s bloky dat, především s poli. Nejjednodušším polem je řetězec znaků zapsaný pomocí "". Například řetězec "ALFA" znamená příkaz, aby překladač v paměti vyhradil 5 bytů a do nich uložil znaky 'A', 'L', 'F', 'A' zakončené 0, tj. standardním terminátorem C řetězce. Výsledkem operace bude pointer typu char * směřující na adresu prvního znaku řetězce. Použijeme-li řetězec k inicializaci pointru: char * str = "ALFA"; // pointer inicializovaný na adresu řetězce dojde k vytvoření dvou paměťových proměnných, které nemusí nutně ležet vedle sebe. Jedna z nich představuje vlastní řetězec znaků a druhá, pointer str typu char * , bude obsahovat adresu prvního bytu řetězce. Řetězec je pointer na datové pole a s tím lze pracovat jako s každou jinou proměnnou. Pozor však na sizeof. Operátor sizeof se vyhodnocuje během překladu a vztahuje se výhradně k vlastnostem proměnné, a ne k jejímu obsahu. printf("%s | %c | %d\n","ABCDEF", *("ABCDEF"), sizeof("ABCDEF") ); // ABCDEF | A | 7 char * abc = "ABCDEF"; // pointer inicializovaný na adresu řetězce printf("%s | %c | %d | %d \n", abc, * abc, sizeof(abc), strlen(abc) ); // ABCDEF | A | 4 | 6 Zatímco v případě, když se operátor sizeof aplikuje přímo na řetězec "ABCDEF", dostaneme hodnotu 7, tj. počet bytů, v nichž je řetězec uložený, použití sizeof na řetězec abc dává výsledek 4, tj. velikost proměnné abc. Chceme-li zjistit délku znakového řetězce, na který pointer abc ukazuje, musí se použít knihovní funkce strlen deklarovaná ve string.h. Ta spočítá počet znaků až k první 0 a v našem případě vrátí hodnotu 6. Pozor, strlen nevrací místo vyhrazené v paměti pro řetězec, pouze spočítá znaky ležící před koncovou 0. Provede-li se příkaz:
38
* abc = 0; // zápis 0 na začátek řetězce printf("%d \n", strlen(abc) ); // 0 pak bude výsledek funkce strlen rovný 0, protože koncová nula se nachází hned na začátku řetězce. Přepíšeme-li omylem koncovou nulu řetězce nějakým nenulovým znakem, bude výsledek strlen předem nedefinovaný. Funkce bude hledat první nulu a dostane-li se přitom mimo přidělený úsek paměti, dojde k chybě narušení paměti. Chceme-li zabránit přepsání řetězce, můžeme použít klíčové slovo const: const char * pc = "ABCDEF"; // pointer na konstantní řetězec znaků * pc = 0; // chyba - nelze modifikovat konstantní objekt Podobné deklarace se často vyskytují v parametrech funkcí. Zdůrazňuje se jimi, že příslušný objekt, na která pointer ukazuje, se pouze čte a není funkcí měněný. Například: char * LimitujDelku( // omez maximální délku řetězce text na počet znaků ve vzor char * text, // do tohoto řetězce bude funkce zapisovat const char * vzor // tento řetězec bude funkce jen číst ) { char * text0 = text; // uložíme si počátek řetězce while(*vzor++ != 0 && *text != 0) text++; // hledáme první 0 v řetězcích *text=0; /* text buď už ukazuje na 0, nebo se musí 0 ukončit. Zápis 0 lze proto provést v obou případech. */ return text0; // Výsledek vracíme jako výstup, aby se funkce dala použít ve výrazech. } Všimněte si, že pointry text a vzor se chovají jako běžné formální parametry funkcí, tzn. funkci se předává jejich kopie, a proto se jejich načítáním nemění originální hodnota argumentu. Dochází pouze k přepsání objektu, na který pointer ukazuje. char * vystup = "123456789"; // pointer ukazující na řetězec znaků printf("%s\n", LimitujDelku(vystup,"ABCDEF") ); // vytiskne se 123456 printf("%s\n", vystup ); // opět 123456 - změnil se jen řetězec, nikoliv obsah pointru vystup Rovněž samotný obsah pointru, tj. v něm uloženou adresu, lze také definovat jako konstantní: char * const cp = "ABCDEF"; // konstantní pointer na řetězec znaků * cp = 0; // v pořádku, cílový objekt lze modifikovat cp = "ALFA"; // chyba - nelze měnit konstantní pointer const char * const cpc = "ABCDEF"; // konstantní pointer na konstantní řetězec znaků * cpc = 0; // chyba - nelze modifikovat konstantní objekt cpc = "ALFA"; // chyba - nelze měnit konstantní pointer 1.4b.1 Operátory [] a + Jednorozměrné obecné pole proměnných se v jazyce C definuje pomocí operátoru []: long data[4]; // pole obsahující 4 členy typu long s indexy 0..3 Tento příkaz znamená pokyn k vyhrazení místa v paměti pro uložení čtyř čísel long. Ty budou mít indexy 0 až 3, protože pole v jazyce C jsou zásadně číslovaná od 0. Identifikátor pole, v našem případě data, představuje konstantní pointer ukazující na začátek pole.
Přepsání hodnoty řetězce definovaného pomocí " " představuje sice povolenou operaci, neboť se jedná o normální paměťovou proměnnou, ale přesto se nedoporučuje podobné příkazy používat. Je-li to nezbytně nutné, pak se program musí překládat s vypnutou podmínkou "Duplicate strings merged". Je-li totiž zapnutá, pak se řetězce se stejným obsahem v přeloženém programu vyskytují pouze jednou bez ohledu na počet jejich výskytů a přepsáním jednoho řetězce se mění obsah všech jeho dalších použití.
39
long data[4];
čtení / zápis dat údaje l ong data[0] ≡ *data
data[1] ≡ *(data+1)
data[2] ≡ *(data+2)
data[3] ≡ *(data+3)
byty paměti data
data+1
data+2
data+3
pointer na prvek pole Obr. 1-9 Pole a operátor [] Uložení do prvku pole s indexem 0, např. data[0] = 100; je zcela ekvivalentní příkazu: ! * data = 100; Zápis práce s jiným indexem, např. data[3] = 300; se rovná *(data + 3) = 100; Operace přičtení čísla 3 k pointru má význam posunu adresy o takový počet bytů, aby ukazovala na prvek pole s indexem 3, tj. na začátek posledního prvku pole data. Operátor [] není vázaný pouze na pole, ale lze ho aplikovat na jakýkoliv pointer s datovým typem. Například: char * text = "abcdefgh"; // pointer inicializovaný na adresu řetězce printf("%c\n", text[3] ); // vytiskne znak 'd', tj. prvek *(text+3) Stejně tak jméno pole představuje pointer a lze ho přiřadit jinému pointru: long * pld ; // pointer typu long pld = data; // přiřazení adresy pole pointru pld pld[2] = 123; // totožná operace jako data[2]=123; nebo *(data+2)=123; Výsledky operací jsou zcela stejné, ať s polem pracujeme přímo nebo prostřednictvím jiného pointru téhož typu. Operaci přičtení nějakého čísla k pointru, můžeme tedy chápat jako relativní posun pointru o příslušný počet prvků v pomyslném poli, jehož členové mají rozměr specifikovaný datovým typem pointru. Analogicky lze rozumět i odčítání a samozřejmě také operacím ++ a -- pro přičtení a odečtení 1. Identifikátor pole má chování odlišné od datových pointrů pouze ve dvou případech. Nesmí stát na levé straně příkazu, a proto jeho hodnotu nelze nikdy změnit, a operátor sizeof na něj aplikovaný vrací počet bytů, které pole zaujímá v paměti. printf("%d | %d | %d\n", sizeof(data), sizeof(data+1), sizeof(pld)); // Výsledek 12 | 4 | 4 Operátor sizeof aplikovaný přímo na identifikátor pole vrací velikost pole v bytech, ale jakákoliv operace s identifikátorem pole dává jako výsledek pouhý pointer s přiřazeným datovým typem, a proto operátor sizeof dává v tomto případě výsledek 4, tj.místo nutné k uložení adresy. Shrneme-li tento výsledek, lze rozdělit pointry v jazyce C na tři základní třídy, které leží ve vzájemné hierarchii a mají následující vlastnosti: 1. pole - adresa + přiřazený datový typ + velikost pole v bytech 2. datový pointer - adresa + přiřazený datový typ 3. void pointer - adresa Použijeme-li terminologii objektů, o nichž budeme hovořit později, můžeme říct, že od void pointru je odvozený datový pointer a od něho zase pole. Pokud přiřadíme pole do datového !
Ekvivalence mezi oběma zápisy vyplývá z toho, že indexy polí se převádějí na operace přičtení čísla k pointru. Výraz: pole[index] se překládá jako: *(pole + index)
40
pointru, dojde k redukci vlastností a zmizí informace o velikosti pole v bytech. Stejně tak, uložíme-li datový pointer do void pointru, ztrácí se informace o přiřazeném datovém typu. short int sidata[100]; // pole obsahující 100 prvků typu short int s indexy 0..99 short int * psi; // pointer s datovým typem short int void * pvoid; // pointer typu void sidata[0]=0; // práce s obsahem pole psi = sidata; // psi ukazuje na pole sidata, ale nemá informaci o jeho délce psi[10]=111; // s obsahem pole lze dále pracovat pvoid = sidata; // pvoid ukazuje na pole sidata, ale určuje jen jeho adresu pvoid[10]=0; // Chyba - void pointer nemá definovaný typ ((short int *) pvoid) [20]=222; // s void pointry lze pracovat pouze po přetypování psi = (short int *) pvoid; // přiřazení směrem výše vyžaduje vždy přetypování sidata=psi; // Chyba - identifikátor pole nesmí stát na levé straně výrazu V uvedené ukázce měly všechny tři pointry sidata, psi i pvoid neustále totožný bitový obsah udávající adresu začátku pole sidata. Při přiřazení se měnily pouze jejich přidružené vlastnosti - datový typ a velikost pole. Převod směrem seshora dolů se prováděl bez přetypování, protože se při něm konala redukce se složitějšího typu na jednodušší, zatímco opačný směr, v našem případě převod z void na datový pointer, si žádal přetypování, neboť se doplňovaly nové vlastnosti. Podobné převody patří mezi běžné operace, zejména v objektech velmi časté, a proto lze čtenáři lze doporučit, aby si uvedenou ukázku dobře prostudoval a vyzkoušel na příkladech. Závěrem této části si ukážeme několik způsobů, jak inicializovat pole. Pro malá pole lze použít přímou inicializaci pomocí seznamu hodnot: int pole0[10] = { 0,1,2,3,4,5,6,7,8,9 }; int pole1[2][3] = { {0,1,2}, {3,4,5} }; Větší pole lze inicializovat zápisem po jednotlivých prvcích a k určení počtu jeho prvků použít výraz stylu sizeof(pole2) / sizeof(*pole2) = celková velikost pole v bytech lomená velikostí jednoho prvku: int pole2[100]; int i; for(i=0; i < sizeof(pole2) / sizeof(*pole2); i++ ) pole2[i] = i; /* Pozor, není možné upravit cyklus a použít v něm pole2[i++] = i; protože překladač nezaručuje pořadí vyčíslování členů výrazů. " */ Předchozí způsob inicializace vyžaduje výpočet indexu v každém kroku cyklu, čili výrazu *(pole2+i). Chceme-li operaci urychlit, můžeme využít pointru, který umí překladač přeložit efektivněji. Rovněž je vhodné upravit i složitý zápis počtu prvků pole pomocí sizeof, který sice cyklus nezdržuje, protože operátor sizeof se vyčísluje při překladu a představuje ekvivalent konstanty, ale složitý zápis snižuje přehlednost programu. Lepší možnost nabízí #define: #define POLE3SIZE 1000 int pole3[POLE3SIZE]; int * pi = pole3; // pointer na začátek pole int j=0; while( j
"
Jazyk C zaručuje pouze pro operátory && a || , že se jejich operandy důsledně vyčíslují zleva doprava. Nezaměňujte memset s funkcí setmem. Obě vykonávají sice stejnou operaci, ale liší se parametry. Funkce setmem existuje v jazyce C pro kompatibilitu s UNIXovými programy. #
41
int pole4[10000]; memset( pole4, 0, sizeof(pole4) ); // zapiš od adresy pole4 byty 0 v počtu sizeof(pole4) Funkci memset nelze samozřejmě použít, pokud prvky pole nemají celočíselný charakter, ale představují například čísla double, protože pak se nula ukládá ve speciálním tvaru závislém na konkrétním zobrazení čísel v pohyblivé řádové čárce. Úplně nakonec ještě jedna ukázka práce s vícerozměrnými poli. Pole se ukládají do paměti po sloupcích a přitom se pravý index pokládá za nejnižší, tj. nejvíce měnící se, čili podobně jako u zápisu čísel. Naproti tomu při čtení nebo zápisu operátorem *, respektivě [], se matice rozkládá odleva, ve smyslu deklarace, a každé použití operátoru znamená přístup na další submatici. Například, operátor [] u identifikátoru dvourozměrné matice, dává jako výsledek pointer na jednorozměrné pole (vektor) obsahující prvky zvoleného sloupce. char matice[2][3][4]; int i,j,k; printf("%d | %d | %d | %d\n", sizeof(matice), // velikost celé matice [2][3][4] = 24 bytů sizeof(*matice), // velikost submatice [3][4] = 12 bytů sizeof(**matice), // velikost submatice [4] = 4 byty sizeof(***matice)); // velikost prvku typu char = 1 byte for(i=0; i<2; i++) for(j=0; j<3; j++) for(k=0; k<4; k++) matice[ i ][ j ][ k ]='A' + k + 4* (j + 3*i); // zapíšeme ASCII znaky vzestupně po sloupcích matice[1][2][3]=0; // uložíme 0 do posledního prvku pole printf("\"%s\"\n", (char *) matice ); /* vytiskneme paměť přidělenou matici jako řetězec bytů, což dá výsledek: "ABCDEFGHIJKLMNOPQRSTUVW" - celkem 23 bytů, neboť ve 24-tém je koncová 0. */
1.4c
Pointry na pointry a na funkce
Pointry nemusejí ukazovat pouze na čísla , ale i na pointry. Při šifrování náročnějších definic pointrů začínejte vždy od * a využijte faktu, že operátor * se sdružuje zprava doleva. Pomocí tohoto pravidla lehce rozluštíme následující ukázky: long cislolong; // proměnná typu long long * plong; // pointer na proměnnou typu long plong = & cislolong; long * * pplong; // pointer na pointer na proměnnou typu long pplong = & plong; long along[10]; // pole deseti čísel typu long along[9] = 999; long * aplong[10]; // pole deseti pointrů na čísla typu long aplong[9]=& cislolong; Pole pointrů se s výhodou používá pro seznam textů, které mají odlišné délky, například: char * chyby[3] = { "Nedostatek paměti pro otevření souboru","Soubor nenalezen","OK"}; představuje úsporné uložení několika indexovaných textů. V samotném poli se uchovávají pouze pointry na řetězce vytvořené překladačem. Pointer na příslušný text se pak získá []: printf( "%s\n", chyby[1] ); // vypíše se text: Soubor nenalezen Kdyby se pole pro uložení textů deklarovalo jako dvourozměrné, musela by se pro velikost jeho sloupců zvolit délka nejdelšího textu a u kratších řetězců, jako "OK", by většina přidělené paměti nevyužila. Jazyk C připouští pointry i na funkce. Z hlediska práce procesoru nejde o nic neobvyklého, adresa funkce je adresa jako kterákoliv jiná. Podobně jako u pole, identifikátor funkce reprezentuje její adresu a lze ho uložit do vhodně definovaného pointru. 42
Zápisy pointrů na funkce jsou již složitější. Využívají se ojediněle. Pomocí nich se například volají, v některých případech, funkce dynamických knihoven, což si ukážeme v kapitole 9.29.2d. void (* pfce)(void); long (* plfce)(void); long (* plfcel)(long); long * (* pplfcepl)(long *);
// // // //
pointer pointer pointer pointer
pfce na funkci tvaru void f1(void); plfce na funkci tvaru long f2(void); plfcel na funkci tvaru long f3(long); pplfcepl na funkci tvaru long * f4(long *);
Ve výše uvedených definicích bylo jméno pointru uzavřeno spolu s operátorem * do závorek, protože operátor (), tj. volání funkce, vyšší prioritu než operátor *. Použití definovaného pointru plfcel ukazuje následující příklad: long moc(long x) // definice funkce pro výpočet 2. mocniny { return x*x; } void main(void) { long l; plfcel = moc; l = ( * plfcel ) (5) ; printf("%ld", l ); }
1.4d
// identifikátor funkce moc má význam její adresy // volání funkce přes pointer s předáním parametru // vypíše se výsledek 25
Operátory new a delete
V moderních programech se pointry používají hlavně pro uložení adres dynamických proměnných, které se alokují z volné paměti na heapu. Operační systémy (OS) mají vestavěné služby alokace paměti, ale jejich použití není většinou výhodné. Win16 přidělují bloky po 8 kB, což znamená, že pokud OS požádáme o 1 byte dynamické paměti, dostaneme ho, ale blokujeme jím celkem 8192 bytů, i když z nich sami využíváme pouze jediný. Ve Win32 je situace jen nepatrně lepší, paměť se přiděluje po 4 kB blocích. Případy, kdy se vyplatí alokace službami OS, budou ukázané později při výkladu specifik Win32. Ve většině případů se vyplatí použít některou interní alokační funkci překladače implementující vlastní správu paměti. Ta si od OS bere celistvé bloky paměti, které pak programu přiděluje po menších kusech. Existuje několik funkcí definovaných v alloc.h, jimiž lze požádat o paměť, ale v C++ se však doporučuje používat přednostně operátory new a delete pro jednotlivé objekty a operátory new [] a delete [] pro alokaci polí. Operátor new provádí alokaci jednoho objektu a operátor delete je jeho protějškem a vrací nepotřebnou paměť, například: struct SPOLE // deklarace tagu struktury { long data[1000]; }; // zcela nutný středník, konec deklarace tagu $
SPOLE * pspole; pspole = new SPOLE;
// definice pointru na strukturu $ // alokace potřebné paměti pro objekt SPOLE
if(pspole != NULL) { memset(pspole, 0, sizeof(*pspole)); pspole->data[999]=999; delete pspole; pspole->data[999]=999; pspole = NULL; }
// test na úspěšnou alokaci // nulování objektu // zápis je ekvivalentní: (*pspole).data[999]=999; // uvolnění paměti // Závažná chyba! Paměť byla již uvolněná! // konstanta NULL specifikuje nealokovaný pointer
$
Deklarace SPOLE a definice pspole se daly rovněž spojit v jediný příkaz: struct SPOLE { long data[1000]; } * pspole;
43
Alokaci paměti provádí operátor new aplikovaný na tag struktury. Vyhradí potřebné místo pro uložení struktury na heapu a vrací buď pointer s datovým typem tagu, pokud se alokace zdařila, nebo v opačném případě, nebylo-li volné místo, dává jako výsledek systémovou konstantu NULL, která je definovaná v řadě include souborů, např. v stdio.h. Častou chybou v programech bývá vynechávání testu na NULL a spoléhání na to, že operační systém disponuje neustále dostatkem paměti. Většinou tomu opravdu tak je, ale v řadě situací může taková slepá důvěřivost operačnímu systému způsobit vážné havárie a doporučuje se vždy provádět kontrolu alokace. Kromě testu na NULL lze dále použít i mechanismus C++ výjimek, jímž disponují vyšší verze překladačů. Ten se bude probírat v kapitole 8.5. (Operátor new metá jako výjimku objekt xalloc). Pointer pspole ukazuje na strukturu a představuje pouhou adresu. Výraz sizeof(pspole) dává hodnotu 4, zatímco * pspole představuje strukturu samotnou, a proto se sizeof(*pspole) rovná počtu bytů, který zaujímá struktura v paměti, a dává stejnou hodnotu jako sizeof(PSPOLE). Přístup na člen struktury nelze zadat výrazem *pspole.data[0], protože operátor tečka má vyšší prioritu než * a my potřebujeme napřed dostoupit na strukturu, na níž ukazuje pointer, a teprve poté na její prvek. Správný příkaz vyžaduje použití závorek, aby se změnilo pořadí operací: (*pspole).data[0] . Takový zápis je ovšem nešikovný, a proto pro něj existuje v jazyce C zkratka ve formě operátoru -> . Výraz ve tvaru X->Y se překládá jako (*X).Y . V okamžiku, kdy nějaká dynamická proměnná není již potřeba, je vhodné uvolnit jí přidělenou paměť, aby se dala použít pro jiné alokace. Následky prohřešků proti tomuto pravidlu závisí na operačním systému. Win32 registrují přidělenou paměť podle procesů. Skončí-li nějaký proces, automaticky se uvolní veškerá jemu poskytnutá paměť, a proto nezrušené dynamické proměnné pouze dočasně snižuje efektivitu práce s pamětí. Naproti tomu, ve Win16 existuje společná správa paměti pro všechny procesy a pokud některý z nich skončí, aniž uvolnil svoje alokované dynamické proměnné, zůstanou jemu přidělené bloky paměti (a to 8 kilobytové) zablokované až do konce běhu Win16. Dynamické proměnné alokované pomocí new ruší příkaz delete. Pozor, po jeho provedení se obsah pointru nezmění a stále obsahuje původní adresu, ale tu mezitím může již používat jiný program nebo samotný operační systém. Jakýkoliv pokus o práci s uvolněnou pamětí bývá proklatě riskantní. Máme-li štěstí, nic se nestane; za horší situace při něm přepíší cizí data a za ještě méně příznivé konstelace bytů narušíme ochranu paměti. Chyby tohoto druhu mají náhodný charakter a velmi špatně se hledají. Má-li pointer delší scope, čím se myslí, že ho lze používat i v následujících úsecích programu, měl by se po zrušení dynamické proměnné, na níž ukazuje, naplnit konstantou NULL, aby se dalo otestovat provedení příkazu delete. Předchozí příklad používal strukturu pro pole, což odpovídá trendu uplatnění objektů. Stejnou operaci lze sice vykonat pomocí typedef, ale s většími obtížemi. Přístup na prvky pole vyžaduje složitější zápis, pro který neexistuje zkratka jako pro dostup na členy struktury. typedef long TPOLE[1000]; // deklarace pole jako typedef (zastaralý způsob) TPOLE * ptpole; ptpole = (TPOLE *) new TPOLE; // přetypování nezbytné, new s typedef nepočítá if( ptpole!=NULL ) // test na úspěšnou alokaci { memset(ptpole,0,sizeof(TPOLE)); (*ptpole)[999]=999; // složitý dostup na člen pole, závorky nezbytné delete ptpole; // uvolnění paměti ptpole=NULL; // informace o tom, že objekt byl už zrušený } V moderních programech se proto typedef deklarace pro dynamicky alokovaná pole uplatňují ojediněle. Pokud se už nechce vytvořit pro pole struktura, bývá vhodnější operátor new []. Ten
44
alokuje pole prvků specifikovaného typu a má stejné chování jako new, tzn. vrací buď pointer adresu nebo konstantu NULL. K němu existuje jako inverzní operátor delete []. Pozor, paměť alokovaná pomocí new [] se vždy musí rušit delete [] , protože operátor delete by u pointeru alokovaného pomocí new[] neuvolnil celou paměť! #define POLELEN 1000 long * plpole; plpole = new long [ POLELEN ] ; // použití operátoru new [] if(plpole!=NULL) // test na úspěšnou alokaci { memset(plpole, 0, POLELEN*sizeof(plpole[0])); // Nulování pole plpole[999]=999; delete [] plpole; // alokace s new [] vyžaduje delete [] plpole=NULL; // informace o tom, že objekt byl už zrušený } S plpole lze pracovat stejně elegantně jako s běžným polem. Jediné komplikace se projeví při zjišťování počtu prvků pole a při ladění programu. Pointer plpole má přiřazený datový typ long, čili ukazuje na jedno číslo typu long, a v C++ ho nelze deklarovat jako náhradní identifikátor pole. Velikost pole plpole v bytech se proto musí počítat jako součin celkového počtu prvků v poli a velikosti jednoho prvku. Při ladění programu nelze pak vypsat obsah celého pole, ale jen plpole[0], protože debugger chápe identifikátor plpole jako pointer na číslo long. Uvedené důvody hovoří pro přednostní užití deklarace struktury pro dynamicky alokovaná pole. Jak uvidíme v kapitolách 2 a 3, takové řešení nabízí i mnohé další výhody. Poznámka: Dynamické alokace by se měly rezervovat pouze pro větší bloky dat anebo pro situace, kdy je potřeba vytvořit objekt s dlouhým duration. Malé proměnné je lepší vytvářet jako lokální, eventuálně jako globální. Lze sice psát programy i takto: char * pstring = new char[10]; // nevhodná alokace malého pole strcpy(pstring,"ALFABETA"); /*.. další operace s pstring...*/; delete [] pstring; Kromě vynechání testu na úspěšnost alokace se kódu dá vytknout zbytečné volání operátoru new. Proč vytvářet pole obsahující pouhých deset znaků a mající krátké duration jako dynamickou proměnnou? Příkaz char pstring[10]; by přece vykonal stejnou službu, aniž by zabíral čas pro alokaci a zvyšoval nároky na správu paměti. V kapitole 8.6 bude předložena ukázka chybné práce s malými alokacemi, při níž se vytvoří dynamické proměnné zabírající několik kilobytů, které zablokují celou volnou paměť. Připravit se několika kilobyty o megabyty paměti, to opravdu není opravdu žádný velký problém.
1.5 Reference Kromě zmíněné práce s dynamickými proměnnými se pointry často používají k předávání parametrů funkcím. Místo velkých bloků dat se přemísťují pouze jejich 4 bytové adresy. Představme si, že v programu deklarujeme tag struktury TRIFAZE pro uložení změřeného napětí na třech fázích vůči zemi a proměnou mereni typu TRIFAZE, která bude inicializovaná třeba na hodnoty 200, 210 a 220. To zapíšeme příkazy: struct TRIFAZE { double faze[3]; }; TRIFAZE mereni = { { 200, 210, 220 } };
45
Vytvořme funkci pro výpočet průměrné hodnoty: double Prumer1(TRIFAZE mer) { return ( mer.faze[0] + mer.faze[1] + mer.faze[2] )/3; } tu zavoláme třeba takto: printf("%lg\n", Prumer1(mereni)); Vytiskne se nám sice správný výsledek 210, ale přesto program není v pořádku. Při volání funkce Prumer1 se předává argument mer přes zásobník. V uvedeném programu se tam ukládá celá proměnná mereni obsahující tři čísla double (hodnota sizeof(mereni) se rovná 24 bytů). Stejnou funkci lze efektivněji napsat pomocí pointrů: double Prumer2(const TRIFAZE * mer) // const zdůrazňuje, že se parametr pouze čte { return ( mer->faze[0] + mer->faze[1] + mer->faze[2] )/3; } Té předáme jako argument adresu proměnné mereni, kterou zjistíme operátorem &: printf("%lg\n", Prumer2( &mereni )); Vytiskne se stejný výsledek, ale při volání funkce Prumer2 se na zásobník ukládá pouze adresa proměnné mereni, tj. 4 bytový pointer typu TRIFAZE. Volání se tak výrazně zrychlilo. Nedostatkem pointrů jako parametrů je ovšem nutnost předávat adresy proměnných a současně komplikovanější práce s pointry. Tyto nedostatky odstraňují reference, pomocí nichž lze funkci napsat elegantněji: double Prumer3(const TRIFAZE & mer) { return ( mer.faze[0] + mer.faze[1] + mer.faze[2] )/3; } Formálním parametrem funkce je proměnná mer typu reference na konstantní strukturu typu TRIFAZE. Funkci Prumer3 voláme klasickým způsobem, tj. jako Prumer1: printf("%lg\n",Prumer3(mereni)); Přitom se však provedou operace jako při funkci Prumer2. Nezkopíruje se tam celá proměnná mereni na zásobník, ale pouze její adresa, čili pointer na mereni. V příkazech funkce Prumer3 pak překladač automaticky provádí potřebné operace pro přístup k objektu na této adrese. Reference lze tedy považovat za jakési automatické pointry, před něž se při kompilaci samočinně doplňují operátory *, resp. &, podle potřeby. V programu s referencemi pracujeme jako s běžnými proměnnými a starost o jejich adresaci přenecháváme překladači. Reference, stejně jako pointry, dovolují psát funkce s vedlejšími efekty, tj. mění hodnoty svých argumentů. Například funkce, která hodnoty obou svých argumentů učiní rovné největšímu z nich, by vypadala takto: void ObeMax(double & d1, double & d2) { if(d1>d2) d2=d1; else d1=d2; } Pro srovnání můžeme tutéž operaci napsat ještě pomocí pointrů: void ObeMaxP(double * d1, double * d2) { if(*d1>*d2) *d2=*d1; else *d1=*d2; }
46
Obě funkce by se v programu použily třeba takto: double da = 100, db=1000; printf("%lg %lg\n", da, db); // výstup 100 1000 ObeMax(da,db); printf("%lg %lg\n", da, db);
// výstup 1000 1000
db=2000; ObeMaxP( &da, &db ); printf("%lg %lg\n", da, db);
// výstup 2000 2000
Reference dovolují na rozdíl od pointrů použít v argumentu číselnou konstantu: ObeMax(da,10000); // místo parametru d2 je číslo 10000 printf("%lg %lg\n", da, db); // výstup 10000 2000 - změnilo se jen da ObeMaxP(&da,20000);
// Chyba! Nelze místo pointru použít číslo.
V případě reference vytvoří překladač dočasnou proměnnou. Do té uloží číslo 10000 a předá pointer na ni funkci ObeMax. Po skončení funkce pomocnou proměnnou zruší. Vzhledem k tomu, že se jedná sice o přípustnou, ale nestandardní operaci, vypíše se varování ve stylu: "Temporary used for parameter d2 in call to ObeMax" = použita dočasná proměnná pro parametr d2 při volání ObeMax. Reference neumožňují změnit přes ně předanou adresu proměnné, a proto znamenají bezpečnější prvek než klasické pointry. Měly by používat přednostně všude, kde je to možné, a nechávat tradiční pointry jen pro nezbytné situace, jako třeba pro dynamické proměnné.
47
2 - Cesta k objektům 2.1 Požadavky na objekty Zamyslíme-li se nad svým přístupem k objektům ve svém okolí, třeba ke strojům, můžeme základní pravidla zacházení s nimi shrnout do sedmi bodů:
1. Objekty vnímáme a používáme jako celistvé útvary, přestože se skládají se z dílčích elementů. Vezměme si například počítač. Ten je sestavený z mnoha jednotek - například z procesoru, operačního systému, myši, klávesnice, monitoru. Všechny tyto elementy jsou samy o sobě složené z ještě menších prvků, a přesto pod slovem „počítač“ nevidíme hromadu součástek pospojovaných drátky, ale celistvý objekt.
2. Objekty lze rozšiřovat o nové funkce. Chceme-li počítač hrající melodie, nemusíme si koupit nový, ale připojíme přídavná zařízení - zvukovou kartu a reproduktory
3. Objektům zadáváme povely pomocí ovládacích prvků a nezasahujeme do jejich vnitřních funkcí. Takové úkony necháváme tvůrcům objektů. Operační systém řídíme pomocí příkazů a nepokoušíme se ho přepisovat nebo upravovat. Např. disk je prostředek pro uložení dat. Pracujeme s ním dle pokynů výrobce a neměníme nastavení jeho parametrů víme, že systémový (low level) formát vede k znehodnocení disku.
4. Nesnažíme se analyzovat objekty a porozumět jim do detailů. Chceme-li používat monitor, nestudujeme chování toku elektronů, který tryská z katody obrazovky, ale spokojíme se s tím, že víme, jak se monitor zapíná a nastavuje jeho jas a kontrast. Kupodivu tento zřejmý fakt porušuje řada programátorů, kteří zkoumají knihovní programy překladače, aby je mohli lépe využívat.
5. Pochopit jednotlivé elementy objektu může být mnohem složitější, než porozumět výsledné funkci objektu. Pokusíme-li se odkrýt činnost počítače přes výzkum jeho součástek, potká nás nepříjemné překvapení. Porozumět činnosti BIOSu, základního programu počítače, nebo procesoru Pentium běžícímu v protected módu, je složitější úkol, než naučit obsluhovat počítač. Půjdeme-li ještě do větší hloubky a budeme-li zkoumat atomy křemíku, abychom odkryli principy moderních součástek, obklopí nás svět kvantové mechaniky, v němž se dovíme, že atomy jsou na pochopení mnohem komplikovanější než svět, který je z nich složený.
6. V objektech existují vnitřní elementy nepřístupné z vyšších úrovní. Právě tyto prvky vyvolávají zmíněný nárůst složitosti při postupu od celku k detailům. Tento jev závisí na komplikovanosti a počtu skrytých elementů v nižší hladině. Sekretářka může počítač na svém stole považovat za hranatou bednu s knoflíky a konektory. Tato jednoduchá představa jí vydrží do doby, než se sundá kryt počítače a pod ním objeví desky s mnoha propojkami a obvody, o jejichž činnosti zhola nic neví. Popravdě řečeno, ve skutečnosti ani nic vědět nechce - k psaní dopisů to nepotřebuje.
7. Objekty vznikají - tj. vyžadují provedení operací, které je připraví k použití. Poté zanikají - tj. přecházejí do neaktivního (pro okolí neškodného) stavu. Vznik a zánik je významnou vlastností celého světa, na níž by mnozí občas rádi zapomněli, což v programování, stejně tak jako jinde, bývá někdy možné. Ale jenom někdy! Objekty programu C++ mají všechny zmíněné vlastnosti a představují logický krok programování, jehož vývoj postupoval od jednolitého programu k rozčlenění na podprogramy a od proměnných přes struktury dat k objektům. Od nich vede přímá spojka k interaktivnímu programování - trendu dneška.
48
Objektové programování v jazyce C, definované normou ANSI C++, vyvinula americká telefonní společnost AT&T (dříve známá jako Bellovy laboratoře) pro programování telefonních ústředen. V následujícím odstavcích zkusíme sledovat cestu, která AT&T dovedla až ke vzniku C++. Na příkladu jednoduchého zásobníku si ukážeme důvody pro použití objektového programování a jeho přednosti.
0.2 Jednolitý program Předpokládejme, že potřebujeme převracet psaná slova. Když napíšeme na klávesnici řádku: „alfa1 beta gama3 “, chceme, aby se vypsala pozpátku jako: „ 1afla ateb 3amag“. Uvedenou funkci lze realizovat pomocí zásobníku: Příklad: Zásobník - verze 0 #include <stdio.h> // základní funkce vstupu a výstupu #include // getch(), putch() #define DELKA_ZASOBNIKU 1000 // délka zásobníku char * pzasobnik; int vrchol;
// pointer na paměť zásobníku // index na první volné místo
int main(void) { char vstup, vystup; //*** Inicializace (konstrukce) zásobníku ************************ pzasobnik=new char[DELKA_ZASOBNIKU]; // alokujeme paměť pro zásobník if(NULL == pzasobnik) return 1; // je-li CHYBA a není paměť pro zásobník, konec. vrchol=0; // umístíme index na začátek paměti zásobníku //*** Práce se zásobníkem **************************************** while((vstup=getch()) != 27) { if(vrchol0) { vystup=pzasobnik[--vrchol]; putch(vystup); } } }
// čteme znaky až ke znaku ESC // zasuň přečtený znak do zásobníku // je-li mezera, pak vytiskni zásobník // dokud je něco v zásobníku, pokračuj // odeber prvek // a vytiskni ho na displej
//*** Likvidace (destrukce) zásobníku ************************* delete [] pzasobnik; // uvolníme paměť return 0; } Zásobník tvoří pole znaků a index směřující na místo pro uložení dalšího prvku: char * pzasobnik; // pointer na paměť zásobníku int vrchol; // index na volné místo Zásobník je inicializován, tj. připraven k práci, příkazem: pzasobnik=new char[DELKA_ZASOBNIKU];
49
Operátor new[] vyhradí z volné paměti souvislý řetězec bytů o délce definované konstantou DELKA_ZASOBNIKU a ukazatel na první byte přidělené paměti uloží do proměnné pzasobnik, typu pointer na char. Není-li dostatek paměti a new[] nám vrátí hodnotu NULL, musíme ukončit program. Příkaz: vrchol=0; umístí index ukládání prvků na začátek zásobníku. Znaky klávesnice čteme funkcí getch() do proměnné vstup ve smyčce while. Dokud jsou znaky různé od ESC ( ASCII hodnota 27), vkládáme je do zásobníku: if( vrchol0) // je-li něco v zásobníku { vystup=pzasobnik[--vrchol]; // odeber prvek putch(vystup); // a vytiskni ho } Opět pro zapomnětlivé - odebírání znaků ze zásobníku: vystup = pzasobnik[--vrchol]; // odeber prvek je funkčně ekvivalentní dvojici příkazů: vrchol = vrchol-1; vystup = pzasobnik[vrchol]; Při ukládání jsme napřed zapsali znak a potom posunuli index, a proto při čtení postupujeme % obráceně - napřed couvneme indexem a pak čteme znak. Způsob, kterým jsme náš program napsali, odpovídá době zrodu počítačů a začínajícímu programátorovi. Obdrželi jsme krátký a funkční program splňující požadavky zadání. Není ale univerzálním blokem stavebnice a veškerou složitost algoritmu soustřeďuje do jednolitého bloku uvnitř funkce main(). Kdybychom takto napsali rozsáhlejší aplikaci, byla by krajně nepřehledná, což se vždy snoubí s vyšší pravděpodobností výskytu chyb.
2.3 Podprogramy Upravme zásobník pomocí podprogramů. Řeknete si možná: „Proč?“. Náš program neobsahuje úseky, které by se opakovaly, a každý jeho příkaz je unikátní. Ano, s tím lze souhlasit, ale s výhradou. Podprogramy, kromě zkrácení kódu, poskytují i další významnou vlastnost - dovolují operace rozčlenit na logické celky, čím se zvýší čitelnost programu. Tu moderní programování řadí na čelní pozici. Při rozkladu algoritmu zásobníku na podprogramy vyjdeme od dat, čili od pointru pzasobnik a proměnné vrchol, indexu jeho vrcholu. Proměnné tvoří vždy základ objektu. Operace, které se s nimi provádějí, patří obvykle do tří hlavních skupiny:
1. Konstrukce a destrukce. Data potřebná pro činnost zásobníku vznikají příkazem alokace paměti, tj. použitím operátoru new[], a dále nastavením indexu vrchol na nulu. Svou existenci končí uvolněním paměti pomocí operátoru delete[]. Tyto operace zahrneme do dvou podprogramů konstrukce a destrukce zásobníku. %
Náš zásobník roste od 0 směrem nahoru a jeho ukazatel vrchol udává volné místo pro zápis dalšího prvku. V tomto směru se chová odlišně od zásobníku procesoru (stack), který roste přesně obráceně, tj. od vyšší adresy k nižší, a jeho ukazatel vrcholu (stack pointer) směřuje na poslední do něho vložený prvek.
50
2. Testy na stav objektu. V našem případě přicházejí v úvahu dva dotazy: „Je zásobník prázdný?“ a „Je zásobník plný?“.
3. Operace s daty v objektu. U zásobníku pracujeme s vrcholem, buď na něj data přidáváme, nebo je z něho odebíráme. Funkce pojmenujeme podle instrukcí procesoru na Push() a Pop(). V programu uděláme, pro zvýšení jeho univerzálnosti, ještě následující úpravy: a. Délka zásobníku nebude již určená konstantou, ale argumentem velikost předaným funkci KonstrukceZasobniku(int velikost). Zadaná velikost se uloží do nové globální proměnné delka. b. Pokud se alokace paměti nepodaří, program se tentokrát neukončí, ale jen se nastaví proměnnou delka na 0. Operace se zásobníkem se pozmění tak, aby ani při nulové délce alokace nezpůsobily chybu narušení paměti. Hlavní program bude moci teď sám rozhodnout, zda chce v případě neúspěšné alokace pokračovat jinými operacemi anebo ukončit svou práci. Zásobník přepíšeme do nového tvaru: Příklad: Zásobník - verze 1 #include <stdio.h> // základní funkce vstupu a výstupu #include // getch(), putch() // data používaná funkcemi zásobníku char * pzasobnik; // pointer na paměť zásobníku int delka; // délka přidělené paměti int vrchol; // index na první volné místo // testy na stav zásobníku int JePrazdny() { return( vrchol==0 ); }
// vrací 1, je-li zásobník prázdný, jinak 0
int JePlny() { return( vrchol==delka ); }
// vrací 1, je-li zásobník plný, jinak 0
// operace s daty v zásobníku void Push(char data) // není-li zásobník plný, přidej do něho data { if( !JePlny() ) pzasobnik[vrchol++] = data; } char Pop(void) // vyjmi data ze zásobníku, je-li prázdný, vrať 0 { return( !JePrazdny() ? pzasobnik[--vrchol] : 0 ); }; // Vznik a zánik proměnných zásobníku void KonstrukceZasobniku(int velikost) { vrchol=0; pzasobnik=new char[velikost]; delka = ( NULL==pzasobnik ) ? 0 : velikost; } void DestrukceZasobniku(void) { if( pzasobnik != NULL) delete [] pzasobnik; }
// alokuj paměť zadané délky // inicializuj index na začátek // přiděl paměť // delka nastavena podle výsledku alokace
// byla alokována paměť, uvolni ji
// Program používající zásobník int main(void) { char vstup, vystup;
51
KonstrukceZasobniku(1000); if( JePlny() ) return 1;
// Vytvoř zásobník pro 1000 znaků /* Je-li zásobník plný hned po vytvoření, pak musí mít délku 0, což znamená, že se nealokovala paměť. Program se ukončí. */ while( (vstup=getch()) != 27) // čteme znaky až ke znaku ESC { Push(vstup); // Přidej znak na vrchol zásobníku. if(vstup==' ') // Je-li stisknula mezera, vytiskni obsah zásobníku, { while( 0!=(vystup=Pop()) ) putch(vystup); } } DestrukceZasobniku(); // Uvolni paměť return 0; // Konec programu } Zhodnotíme-li první verzi programu, můžeme její hlavní rysy, kladné (+) a záporné (-), vyjádřit body:
1. (-) Použití podprogramů prodloužilo kód. 2. (-) Program stále není univerzálním modulem. Budeme-li potřebovat dva zásobníky, musíme duplikovat všechny funkce včetně dat, která používají, a vhodně je přejmenovat.
3. (+) Program je rozčleněn na samostatné bloky. Lze ho proto snadněji ladit a upravovat. 4. (+) Hlavní část programu funkce main() je mnohem přehlednější a činnost programu lze vidět na první pohled.
2.4 Struktury Pokusme se teď upravit program pro dva nezávislé zásobníky - do jednoho se budou ukládat čísla a do druhého ostatní znaky. Přijde-li mezera, vytisknou se napřed čísla a potom zbylé znaky, oboje v obráceném pořadí. Vstup z klávesnice ve tvaru: „123Alfa G4a5mma “ by tedy měl za následek výpis: „ 321aflA 54ammaG“. Neobjektové C nám nabízí datovou strukturu (klíčové slovo struct ). Pomocí té lze svázat všechny deklarace dat potřebných pro zásobník pod společný identifikátor, třeba ZASOBNIK. Struktura dovoluje vytvořit libovolné množství nezávislých zásobníků. Všem funkcím, které se zásobníkem pracují, se musí ale předávat jako vstupní argument proměnná typu pointer na & strukturu ZASOBNIK. Ten pojmenujeme pZAS a přes něj se bude pracovat s prvky struktury. Poznámka: Místo pointru by bylo samozřejmě výhodnější předávat reference na strukturu zásobník. Důvody, proč jsme to neudělali, vyplynou z dalšího výkladu. Příklad: Zásobník - verze 2 #include <stdio.h> // základní funkce vstupu a výstupu #include // getch(), putch() #include // isdigit() struct ZASOBNIK { char * pzasobnik; int delka; int vrchol; };
// deklarace struktury- tag // pointer na paměť zásobníku // délka přidělené paměti // pointer na paměť zásobníku
// testy na stav zásobníku int JePrazdny(ZASOBNIK * pZAS) { return(pZAS->vrchol==0); } int JePlny(ZASOBNIK * pZAS) { return(pZAS->vrchol==pZAS->delka);} &
Připomínám, že výraz: pZAS->vrchol znamená v jazyce C zkratku pro zápis: (*pZAS).vrchol
52
// operace s daty v zásobníku void Push(ZASOBNIK * pZAS, char data) // zasuň data do zásobníku { if(!JePlny(pZAS)) pZAS->pzasobnik[pZAS->vrchol++]=data; } char Pop(ZASOBNIK * pZAS) // vyjmi data, vrať 0, je-li zásobník prázdný { return( !JePrazdny(pZAS) ? pZAS->pzasobnik[--(pZAS->vrchol)] : 0 ); } // Vznik a zánik zásobníku void KonstrukceZasobniku(ZASOBNIK * pZAS, int velikost) { pZAS->vrchol=0; // umísti index na začátek zásobníku pZAS->pzasobnik= new char[velikost]; // přiděl paměť pZAS->delka = ( NULL==pZAS->pzasobnik ) ? 0 : velikost; } void DestrukceZasobniku(ZASOBNIK * pZAS) { if( pZAS->pzasobnik!=NULL ) delete [] pZAS->pzasobnik; } int main(void) { char vstup, vystup; ZASOBNIK cisla, necisla; KonstrukceZasobniku(&cisla,500); if( JePlny(&cisla) ) return 1; KonstrukceZasobniku(&necisla,1000); if( JePlny(&necisla) ) { DestrukceZasobniku(&cisla); return 1; };
// Vytvoření dvou zásobníků // Inicializuj zásobník pro čísla // Je-li zásobník plný hned po vytvoření, // pak má délku 0, tj. není paměť. // Inicializuj zásobník pro ostatní znaky // ? Test, je-li zásobník vytvořen /* Došlo-li k chybě při vytváření druhého zásobníku, musíme před ukončením programu napřed zrušit první již vytvořený zásobník pro čísla. */ // Vrať chybový kód
// Práce se zásobníkem while((vstup=getch()) != 27) { if( isdigit(vstup) ) Push(&cisla, vstup); else Push(&necisla, vstup); if(vstup==' ') { while(0!=(vystup=Pop(&cisla))) putch(vystup); while(0!=(vystup=Pop(&necisla))) putch(vystup); } }
// čteme znaky až ke znaku ESC // Test, je-li vstup číslem, tj. znak ‘0’ až ‘9’ // Je-li mezera, vytiskni celé zásobníky // tisk zásobníku čísel // tisk ostatních znaků
// Destrukce obou zásobníků DestrukceZasobniku(&cisla); DestrukceZasobniku(&necisla);
53
return 0; } // Konec funkce main
// Konec programu
Program verze 2 lze hodnotit rozporuplně. Napišme si jeho hlavní rysy, kladné (+) a záporné (-), v bodech:
1. (+) Podprogramy pracující se zásobníkem mají charakter univerzálního kódu, který lze bez úprav používat i v jiných programech.
2. (-) Nutné odkazy na datové struktury přes pointer ZASOBNIK * pZAS prodloužily program a snížily jeho přehlednost.
3. (-) Neexistuje spojení mezi daty a funkcemi, které s nimi pracují. Příslušné vazby si musíme pamatovat. Zmýlíme-li se, snadno způsobíme těžko odhalitelné chyby.
4. (-) Každé funkci musíme pracně předávat pointer na datovou strukturu. 5. (-) Můžeme vytvořit libovolné množství zásobníků, ale každý musíme sami inicializovat a opět zrušit. Pokud to opomineme, způsobíme tím vážnou chybu. Když k tomto bodu dospěli programátoři AT&T při vývoji složitých programů pro telefonní ústředny, pokusili se najít řešení. Vycházeli přitom z faktu, že princip svázání všech logicky spojených dat do jedné struktury je správný, protože dovoluje vytvářet univerzální programové moduly. Kazí ho jen nutnost předávat funkcím pointer na data a pomocí něho složitě přistupovat na členy struktury. Proč by takovou věc nemohl za nás dělat překladač?
2.5 Objektový program V této části vyřešíme dva problémy: a) automatické předávání pointru funkcím pracujícím se zásobníkem; b) automatické volání operací nutných pro konstrukci a destrukci zásobníku. Učiňme vazbu mezi funkcí a datovou strukturou. Podobně jako deklarujeme data struktury uvnitř jejího bloku, zapišme tam i funkci, která s nimi pracuje. Té budeme automaticky předávat pointer na strukturu, uvnitř které je definována. Tento pointer se jmenuje this. Například funkce JePlny() mající v předchozím příkladu tvar: int JePlny(ZASOBNIK * pZAS) { return(pZAS->vrchol==pZAS->delka); } se po definici uvnitř struktury změní na tvar: struct ZASOBNIK // popis tvaru struktury, tzv. tag { char * pzasobnik; // pointer na paměť zásobníku int delka; // délka přidělené paměti int vrchol; // pointer na paměť zásobníku int JePlny(void) { return( this->vrchol == this->delka ); } }; Máme-li v programu definované dvě struktury: ZASOBNIK cisla, necisla; potom funkci JePlny() zapsanou uvnitř struktury voláme analogicky přístupu k elementům strukturu a to: cisla.JePlny(), resp. necisla.JePlny(). Funkce členy struktury budeme nazývat metodami třídy, abychom je odlišili od ostatních funkcí. Uvedení metody uvnitř struktury specifikuje jen to, že se této metodě předává pointer this. Její kód se nachází vždy mimo objekt. Když jsme definovali dva objekty typu struktura ZASOBNIK - cisla a necisla, pak každý v paměti zaujímal pouze místo nezbytné pro uložení svých datových členů, tj.pointru pzasobnik a dvou čísel delka a vrchol, což reprezentuje
54
celkem 12 bytů (ve Win32). Jinými slovy, každý objekt typu ZASOBNIK sdílí kód metod s ostatními objekty téhož typu, ale má svoje vlastní datové členy. ' Volání metody JePlny() se nám zjednodušilo, ale stále odkazujeme na data struktury přes pointer this, i když automaticky předávaný. Nešlo by s tím také něco udělat? Jistou možnost nabízí viditelnost proměnných (visibility), o níž se hovořilo v úvodní části těchto skript. 2.5a Viditelnost prom ěnných a m etod C++ nabízí možnost psát místo výrazu this->vrchol pouze jméno proměnné vrchol, ke kterému si překladač si sám doplní this->. Pro určení, které proměnné metody se mají adresovat jako data struktury a které ne, se překladač řídí pravidly viditelnosti probranými v předchozích částech (str. 13). Pro identifikátory použité v metodách struktur se pravidla viditelnosti dají shrnout zhruba do kroků (podrobněji až v kapitole 3.7):
1. Identifikátor se napřed hledá mezi lokálními deklaracemi, buď mezi argumenty metody nebo jejími proměnnými.
2. Není-li identifikátor nalezený podle bodu 1, hledá se mezi prvky struktury, v níž je metoda deklarovaná. Přitom se vychází z faktu, že všechny členy struktury, tj. data i metody, mají scope struktury. Existují tedy všechny najednou už při úvodní { závorce deklarací, a proto se dá odkazovat i na členy deklarované později.
3. Nebyl-li identifikátor nalezený v bodě 1 a 2, prohledávají se globální objekty. 4. Nahlásí se chyba. Je-li některý identifikátor překrytý jinou deklarací, ale stále má platný scope, lze ho určit pomocí už probraného operátoru :: . Specifikuje-li se před ním název struktury (třídy), znamená to hledaní skrytého identifikátoru ve scope příslušné struktury či třídy. Například zápis: ZASOBNIK::delka udává proměnnou delka definovanou ve struktuře ZASOBNIK. Operátor :: bez jména struktury odkazuje na globální deklaraci, jak bylo už ukázáno dříve. Celý postup nejlépe přiblíží následující příklad: /*1*/ int A;
// globální proměnná A
/*2*/ struct VID { /*3*/ void M() { A=1;
// deklarace struktury
VID::A=2; this->A=3; /*4*/
/*5*/
'
::A=4; int A; A=4; VID::A=5; this->A=6; } int A; };
// metoda struktury /* proměnná A třídy definovaná v příkazu [5] - všechny proměnné a metody struktury mají scope třídy a nezáleží na pořadí jejich deklarací */ // opět A z [5] , operátor :: lze použít, ale zde je nadbytečný. /* znovu A z [5] , odkaz přes pointer this, který se automaticky předává metodám třídy, je sice povolený, ale zde nadbytečný, angl. "obsolete" */ // globální A z [1] /* Lokální definice proměnné A překryla A z příkazu [5]. To je teď neviditelné, ale má stále platný scope */ // proměnná A třídy definovaná v příkazu [4] // dostup na skryté A z [5] , zde je operátor :: zcela nutný // jiný možný dostup na A z [5] // Konec těla metody C() ---------------------------------// Data struktury // Konec deklarace struktury VID
Existuje výjimka z tohoto pravidla - data typu static, ale o nich až v další kapitole.
55
void VnejsiFunkce(void) { /*6*/ VID vid; // definice proměnné vid /*7*/ VID * pvid = &vid; // definice pointru pvid vid.A=1; /* do členu A proměnné vid definované na [6] ulož číslo 1 */ vid.VID::A=1; // jiný zápis předchozí operace, VID:: je tady navíc vid.A=::A; // do prvku A struktury [6] ulož globální A [1] vid.M(); // aplikace metody [3] na data struktury [6] vid.VID::M(); // jiný zápis předchozí operace, VID:: je navíc pvid->M(); // aplikace metody [3] na strukturu na níž ukazuje pointer [7] pvid->VID::M(); // jiný zápis předchozí operace, VID:: je navíc /*!!*/ VID::M(); /* CHYBA: metodu třídy lze volat z vnější funkce výhradně prostřednictvím definovaných (tj. existujících) dat struktury */ /*!!*/ VID::A=0; /* CHYBA: lze pracovat jen s daty existujících objektů. VID znamená pouze deklaraci vnitřní organizace struktury. Ta nezabírá žádné místo v datové paměti, a proto do ní nelze nic uložit. */ /*!!*/ this->A=0; // CHYBA: pointer this je dáván jen metodám };
2.5b
Konstruktory a destruktory
Další bod, potřebující změnu, představují operace prováděné při vzniku a zániku každého objektu, čili na začátku a konci jeho duration. I tyto úkony může překladač zařazovat automaticky. C++ dovoluje popsat operace, které jsou nutné pro vznik a zánik objektu ve speciálních metodách nazvaných konstruktory a destruktory. Konstruktor se deklaruje jako metoda mající název shodný s tagem struktury. Pro náš zásobník to bude metoda: ZASOBNIK(int velikost); Destruktor má také identifikátor jako tag struktury, ale před něj je předřazený znak ~. Pro zásobník bude mít destruktor tvar: ~ZASOBNIK(); Konstruktory a destruktory mají následující vlastnosti:
1. Nesmějí vracet žádné parametry, dokonce ani typy void. 2. Metoda konstruktoru může mít formální argumenty. Má-li je, píšeme je do závorek za jméno proměnné. Například: ZASOBNIK cisla(100); určuje vytvoření datové struktury cisla typu ZASOBNIK s předáním hodnoty 100 v argumentu jejímu konstruktoru. Kdybychom napsali jen: ZASOBNIK cisla; znamenalo by to, že chceme volat konstruktor bez parametrů, a pokud takový není definovaný, objevilo by se chybové hlášení: „Chybí konstruktor ZASOBNIK().“
3. Strukturu, která má definovaný konstruktor s jedním parametrem, lze vytvořit i zápisem: ZASOBNIK cisla = 100; který je plně rovnocenný tvaru se závorkami, tj.: ZASOBNIK cisla(100);
4. Konstruktor nemůžeme volat sami, jeho zařazení do kódu závisí výhradně na překladači, který ho vloží při deklaraci příslušné struktury.
5. Můžeme definovat víc konstruktorů, které se od sebe vzájemně liší použitými argumenty (o tom více části o přetěžování funkcí a metod - kapitola 3.2).
6. Destruktor nemá žádné formální argumenty a je vždy pouze jediný. 7. Destruktor lze volat jako běžnou metodu, ale s výjimkou speciálních situací se to nedělá. Vložení operací pro zrušení objektu se nechává na překladači.
Jedinou výjimkou z tohoto pravidla jsou data typu static, o nichž budeme hovořit později.
56
8. Pokud neuvedeme metody konstruktoru nebo destruktoru, překladač místo nich vytvoří náhradní, které vytvoří či zruší objekt, ale neprovádějí žádné inicializace proměnných.
9. Pokud vytváříme anebo rušíme dynamický objekt, potom pouze operátory new a delete zaručují volání příslušných konstruktorů a destruktorů.
10. Překladač vytváří dva výchozí (default) konstruktory: a) konstruktor bez argumentů. Ten existuje pouze tehdy, když jsme žádný jiný konstruktor nedefinovali; b) copy konstruktor: TRIDA(TRIDA & ExistujiciTrida), který slouží k inicializaci z dat existující třídy stejného typu. Copy konstruktor bude vytvořen automaticky, je-li použitý v programu a přitom není ve třídě definovaný jeho ekvivalent. Nyní může přepsat program pro zásobník do nového tvaru pomocí pravidel pro překlad jmen a s využitím konstruktorů a destruktorů: Příklad: Zásobník - verze 3 - téměř objektový program #include <stdio.h> // základní funkce vstupu a výstupu #include #include struct ZASOBNIK { char * pzasobnik; int delka; int vrchol;
// deklarace organizace struktury // pointer na paměť zásobníku // délka přidělené paměti // pointer na paměť zásobníku
// metody zásobníku int JePrazdny() { return( vrchol==0 ); } int JePlny() { return( vrchol==delka ); } void Push(char data) { if( !JePlny() ) pzasobnik[vrchol++]=data; } char Pop(void) { return( !JePrazdny() ? pzasobnik[--vrchol] : 0 ); } // konstruktor = operace vyvolané automaticky při vzniku zásobníku ZASOBNIK(int velikost) { vrchol=0; // index na začátek pzasobnik=new char[velikost]; // přidělení paměti delka = (pzasobnik==NULL) ? 0 : velikost; // nastav délku paměti } // destruktor = operace vyvolané automaticky při rušení dat zásobníku ~ZASOBNIK() { if( pzasobnik!=NULL ) delete [] pzasobnik; }; };
57
int main(void) { char vstup,vystup; ZASOBNIK cisla(100), necisla(200); // konstrukce zásobníku if( cisla.JePlny() || necisla.JePlny() ) // test vzniku zásobníků return 1; /* Nezdařila se alokace paměti, konec programu. Destruktory ~ZASOBNIK() se volají automaticky */ while((vstup=getch()) != 27) // čteme znaky až do znaku ESC { if(isdigit(vstup)) cisla.Push(vstup); else necisla.Push(vstup); if(vstup==' ') { while(0!=(vystup=cisla.Pop())) putch(vystup); while(0!=(vystup=necisla.Pop())) putch(vystup); } } return 0; // Destruktory ~ZASOBNIK() se volají automaticky } Program má skoro všechny požadované přednosti. Jeho délka odpovídá přibližně neobjektovému programu verze 0. Na rozdíl od něho, ale představuje univerzální modul použitelný bez úprav i v jiných programech. Kromě toho vypadá přehledně. Právě kvůli této poslední vlastnosti, dobré čitelnosti kódu, se vyplatí psát objektově i jednoduché a jednoúčelové operace.
2.6 Atributy přístupu Našemu zásobníku se dá už vytknout jedině to, že neskrývá vnitřní metody struktury a data. Představte si, že v programu omylem napíšeme příkaz: cisla.delka++; , kterým zvětšíme údaj velikosti paměti vyhrazené pro zásobník o jeden byte. Ve většině případů se nic nestane, protože zásobník je málokdy naplněn celý. Pokud by k tomu ale došlo, selže test odvozený od hodnoty delka a přepíše se paměť ležící za úsekem vyhrazeným pro zásobník. Co se pak přihodí, závisí na tom, kdo a jak používá narušenou oblast. Třeba se nám z ničeho nic zhroutí program, ale pak poběží celé měsíce bez poruch. Chybný příkaz způsobil těžko odhalitelnou chybu zejména tehdy, když struktura ZASOBNIK tvoří nepatrnou částečku rozlehlého programu. C++ zavádí možnost omezit dostup k jednotlivým proměnným z vnějšku a definuje pro to tři atributy přístupu public, protected a private. Metody struktury mají přístup ke všem prvkům své struktury bez ohledu na jejich i svůj atribut přístupu, ale ostatní funkce smějí používat jen metody a data s atributy public. Ukážeme si to na příkladu: struct TEST { private: int dpri; int fpri(void) { return 1; } protected: int dpro; int fpro(void) { return 2; } public: int dpub; int fpub(void) { return 3; }
// všechny následující členy budou private
// změna - následující členy budou protected
// změna - následující členy budou public
58
void TestMetody(void) { dpri=0; dpro=0; dpub=0; fpri(); fpro(); fpub(); } private: void TestMetody1(void) { dpri=0; dpro=0; dpub=0; }; void main(void) { TEST tst; tst.dpub=0; tst.dpri=0; tst.dpro=0; tst.fpri(); tst.fpro(); tst.fpub();
// Kterákoliv metoda třídy // má dostup na všechna její data // a smí volat kteroukoliv její metodu. // Následující členy budou private // Každá metoda smí pracovat se vším ze své struktury fpri(); fpro(); fpub(); }
/* Z vnějších funkcí (to je z těch, které nejsou součástí třídy), jsou přístupná jen data public */ // !! CHYBA: "dpri is not accessible in function main()" // !! CHYBA: "dpro is not accessible in function main()" // !! CHYBA: "fpri() is not accessible in function main()" // !! CHYBA: "fpro() is not accessible in function main()" /* Z vnějších funkcí lze volat metody public */
} Uvedený příklad ukazuje, že private a protected členy se chovají totožně a jsou přístupné jen metodám vlastní třídy. Rozdíl mezi nimi se uplatňuje až při odvozování (dědění) dalších objektů. To bude tématem kapitoly 3.6. POZOR! Konstruktory a destruktory je vhodné zařadit do sekce public, jinak náš objekt nemůžeme vytvořit. Konstruktory a destruktory nikdy nejsou private, ale výjimečně lze u nich užít atributy protected. Taková třída slouží výhradně jako základ pro odvození jiných objektů a nelze ji používat samostatně. Dosud jsme atributy přístupu nepoužívali a všechny naše prvky měly přístup public, což je výchozí (default) přístup pro popis struct. To znamená, když omylem nedeklarujeme ve struct atribut přístupu, pak lze pracovat se všemi jejími prvky bez omezení. Z tohoto důvodu C++ zavádí nový typ class, který se od struct liší pouze tím, že má jako výchozí hodnotu atributu přístupu nastaveno private. Až na rozdíl ve výchozí hodnotě atributu přístupu jsou struct i class zcela záměnné. Atributy přístupu sice není nutné používat, ale přesto se to doporučuje pro zlepšení kontroly kódu. Při tvorbě objektu se často začíná tak, že se všem datům přiděluje atribut private a metodám zase public. Podle potřeby se pomocné metody, které není vhodné volat zvnějšku, předefinují jako private, a naopak nezbytně nutná data, na něž se musí přistupovat přímo se označí jako public. Objektové programování dává přednost tomu, aby obsah dat třídy měnily výhradně její metody, protože ty lze lépe kontrolovat než obyčejné zápisy do proměnných. Přepracujeme náš příklad s použitím atributů přístupu do konečné podoby a současně rozdělme program na dva soubory: HLAVNI.CPP - vlastní kód používající zásobník ZASOBNIK.H - prototyp header vkládaný příkazem preprocesoru #include do všech programů, v nichž je zásobník potřeba. Příklad: Zásobník - výsledný objektový program // ======= "ZASOBNIK.H" ============================== #ifndef _ZASOBNIK_H_ #define _ZASOBNIK_H_
// podmíněný překlad není-li definován symbol // definuj symbol pro prevenci opětovného vložení
59
class ZASOBNIK { char * pzasobnik; int delka; int vrchol;
// // // //
přístup nastaven na private - výchozí hodnota pointer na paměť zásobníku délka přidělené paměti pointer na paměť zásobníku
public: // přístup změněn na public int JePrazdny() { return(vrchol==0); } int JePlny() { return(vrchol==delka); } int Vrchol() { return vrchol; } // cteni private elementu, bude potřeba až v dalších příkladech void Push(char data) { if( !JePlny() ) pzasobnik[vrchol++]=data; } char Pop(void) { return( !JePrazdny() ? pzasobnik[--vrchol] : 0 ); } ZASOBNIK(int velikost) // konstruktor { vrchol=0; // umísti index na začátek pzasobnik=new char[velikost]; // přidělení paměti delka = (pzasobnik==NULL) ? 0 : velikost; // nastav délku paměti } ~ZASOBNIK() { if(pzasobnik!=NULL) delete [] pzasobnik; } // destruktor }; #endif // ======= "HLAVNI.CPP" ============================== #include <stdio.h> // základní funkce vstupu a výstupu #include #include #include "ZASOBNIK.H" int main(void) { char vstup, vystup; ZASOBNIK cisla(100), necisla(200); if( cisla.JePlny() || necisla.JePlny() ) // Test alokace paměti return 1; // není paměť, konec programu while((vstup=getch()) != 27) // čteme znaky až do znaku ESC { if( isdigit(vstup) ) cisla.Push(vstup); else necisla.Push(vstup); if(vstup==' ') { while( 0!=(vystup=cisla.Pop()) ) putch(vystup); while( 0!=(vystup=necisla.Pop()) ) putch(vystup); } } return 0; } /* HÉURÉKA ! Objektový zásobník jest hotov. */
2.7 Jak si vytvořit vlastní objekt Předchozí postup - vytvoření nové třídy postupnou transformací obyčejného programu přes pointry k objektům - sloužil pouze pro výklad. Třídy píšeme přímo ve finálním tvaru. Jak při tom ale postupovat? Základ objektu tvoří vždy data. Kvůli tomu se to také nazývá objektové programování. První úvaha vždy zní - jaké proměnné budou potřeba k uložení dat? Pokud to již víme, následuje další krok a to vytvoření konstruktoru, který proměnné inicializuje, eventuálně destruktoru, kterým celý objekt rušíme. Nakonec přicházejí metody, s nimiž se pracuje s objektem. Ukažme si celý postup na jednoduchém příkladu. Předpokládejme, že potřebujeme vytvořit objekt pro výpočet průměru z nějakých naměřených údajů, které postupně přicházejí na náš počítač. 60
Otázka 1. - Jaké proměnné budeme potřebovat? Pro postupný výpočet průměru z posloupnosti údajů potřebujeme celkový součet a počet sečtených hodnot, čili dvě proměnné. S daty by, pokud možno, měly pracovat výhradně metody třídy, a proto oběma proměnným přidělíme atribut private a učiníme z nich prvky třídy, které dáme tag PRUMER. class PRUMER { // atribut přístupu je private, výchozí hodnota pro class double soucet; long pocet; }; Otázka 2. - Které operace jsou nezbytné pro inicializaci proměnných? Pro výpočet průměru musíme inicializovat obě proměnné na hodnoty 0, což zapíšeme do konstruktoru. Jeho identifikátor má shodné jméno s názvem třídy a měl by se nacházet v sekci public, aby se objekt dal vytvořit. class PRUMER { // atribut přístupu je private, výchozí hodnota pro class double soucet; long pocet; public: // změna atributu přístupu na public PRUMER() { soucet=0; pocet=0; } // konstruktor }; Otázka 3. - Které operace jsou nutné při rušení objektu? Objekt pro výpočet průměru nepotřebuje žádné operace pro svou likvidaci a není nutné psát destruktor. Ten bývá zpravidla potřebný pouze tehdy, pokud se vytvářejí dynamické proměnné. V našem případě ho můžeme zcela vynechat a nebo vložit do sekce public vložit prázdný destruktor. Ten vypadal by takto: ~PRUMER() { } Vybereme si obvyklejší cestu a to vynechání destruktoru. Otázka 4. - Jaké operace budeme provádět s objektem? Každou dílčí operaci popíšeme jednou metodou. Při jejich volbě nám pomůže úvaha, že metody se zpravidla dělí do dvou hlavních skupin: A. zjištění okamžitého stavu objektu - v našem případě hodnoty průměru. Tuto metodu nazveme Prumer. B. změna stavu objektu - zde přidání dalšího prvku k dílčímu součtu, tuto metodu nazveme Pricti. Metody, které potřebujeme, přidáme do deklarace třídy PRUMER a to do sekce public. Tím dostaneme výslednou verzi objektu: Příklad: Třída PRUMER class PRUMER { double soucet; long pocet; public: PRUMER() { soucet=0; pocet=0; } double Prumer() { return pocet>0 ? soucet/pocet : 0; } void Pricti(double cislo) { soucet += cislo; pocet++; } }; 61
Objekt je hotový. Vyzkoušíme testem složeným z čtení čísel z klávesnic a počítání jejich okamžitého průměru. #include <stdio.h> // deklarace scanf a printf void main(void) { double vstup; PRUMER sum; while( 0 < scanf("%lg", &vstup) ) // Dokud není chyba, čti double čísla z klávesnice { sum.Pricti(vstup); // Přičti číslo printf("Prumer = %lg\n",sum.Prumer()); // Vytiskni dílčí průměr } printf("Konec.\n"); } Funkce scanf čte řádky z klávesnice a převádí jejich text na čísla. Tvar jejího formátovacího řetězce se velmi podobá printf. V argumentech se jí předávají pointry na proměnné, do nichž zapisuje výsledky převodu a jejichž počet a typy musejí přesně souhlasit se zadaným formátem. Funkce scanf vrací počet přečtených, převedených a uložených polí, což v našem případě bude buď 1, bylo-li napsáno číslo, anebo 0 při chybném vstupu. Cyklus while se tedy ukončí napsáním libovolného nenumerického znaku, třeba lomítka '/'. Objekt PRUMER lze samozřejmě rozšiřovat o další metody jako nulování, čtení počtu prvků. Mohou se k němu přidat i další proměnné a dá se počítat třeba i odhad střední kvadratické odchylky, extrapolace budoucí hodnoty a jiné veličiny. Podobné úpravy si můžete zkusit na procvičení práce s objekty.
62
3 - Více o objektech 3.1 Metody inline V příkladě zásobník verze 4 máme všechny metody popsány v těle třídy, což by u velkých objektů s desítkami metod snižovalo přehlednost zdrojového kódu. C++ umožňuje definovat metodu i vně deklarace třídy, stačí, když uvnitř třídy uvedeme její hlavičku. Úplnou definici metody umístíme na jiné místo programu a před její identifikátor zadáváme operátorem :: název třídy, aby překladač věděl kam kód metody patří. Ukažme si to na příkladu: class TEST { // atribut private - výchozí hodnota pro class int data; public: // změna atributu přístupu na public void Zapis1(int hodnota) // celá metoda uvnitř třídy { data=hodnota; }; void Zapis2(int hodnota); // ve třídě uvedena jen hlavička } TEST::Zapis2(int hodnota) // dodatečná (vnější) deklarace { data=hodnota; } Obě funkce, Zapis1 i Zapis2, provádějí stejnou operaci a z hlediska výsledné funkce programu je lhostejné, jestli definujeme metodu celou uvnitř třídy, nebo tam deklarujeme pouze její hlavičku a úplnou definici umístíme mimo třídu. Odlišnost obou zápisů se projeví pouze způsobem překladu. Za určitých podmínek, o nichž bude hovořeno dále, nabízí C++ pro metody definované uvnitř tříd možnost inline překladu, neboli přímého vložení kódu. Máme-li někde v programu příkazy: TEST test; test.Zapis1(5); test.Zapis2(7); budou při inline překladu kompilované ve skutečnosti jako instrukce: TEST test; test.data=5; test.Zapis2(7); Zatímco Zapis2 se překládá voláním funkce, Zapis1 se převedl na příkazy a vzhledem k tomu, že se jedná o expanzi metody, operace test.data=5; se vykoná, i když data mají atribut private. Poznámka: Expanzi inline je možné vypnout pro všechny metody, pokud zatrhneme v podmínkách překladu „out-of line inline functions“. U překladačů Borland C++ to lze navíc i ve zdrojovém kódu měnit příkazy: #pragma option -vi // zapnutí překladu inline funkcí #pragma option -vi// vypnutí překladu inline funkcí Příkazy se musí vložit mezi deklarace jednotlivých funkcí, (tam, kam dáváme globální proměnné), jinak jsou ignorované. Pokud je expanze zapnutá, pak se tak automaticky překládají všechny metody definované celé uvnitř třídy. Může nastat situace, že metoda, kterou chceme překládat jako inline, je příliš dlouhá a nechceme ji pro přehlednost uvést celou uvnitř třídy, ale mimo ni. V tom případě užijeme klíčové slova inline. Ve třídě definujeme pouze hlavičku metody a úplnou definici umístíme mimo ni.
Pozor! Nezaměňovat inline překlad s příkazem #pragma inline . Ten se přes shodu jmen vztahuje k jinému případu - informuje překladač, že kód obsahuje vložené instrukce asm. Každou funkci lze definovat jako inline, nejen metody třídy. Viz část o linkage - str. 16. Dále některé systémové funkce, zejména pro práci s řetězci, mají též možnost inline překladu, blíže viz. C++ help k #pragma intrinsic.
63
Rozkládáme-li program na více souborů, vnější metoda inline musí ležet vždy v souboru typu header (*.H), aby překladač měl k dispozici kód metody pro vložení- viz. kapitola 1.21.2d. Pokus o zařazení vnějších inline metod do části kódu (*.CPP) vyvolá chybu během sestavování přeložených OBJ souborů. Program linker nahlásí, že mu chybějí definice pro jména inline metod Ne všechny metody se však dají definovat jako inline. Překladač odmítne inline kompilaci, pokud kód obsahuje while, do, for nebo skoky switch, goto či složité konstrukce if, eventuálně příliš mnoho lokálních datových deklarací. Nehlásí chybu, ale pouze varování, že inline požadavku není možné vyhovět. Ukažme si to na příkladu, ve kterém pro ilustraci rozdělíme náš program do tři souborů: TEST.H - hlavička třídy TEST, vkládaný soubor TEST.CPP - definice vnějších metod třídy test HLAVNI.CPP - program používající třídu TEST V příkladu použijeme i inicializace argumentů. Ty lze u metod provádět stejně tak jako u každé funkce jazyka C++, protože metody tříd se od běžných funkcí liší pouze automatickým předáváním pointru this a platí pro ně běžná pravidla jako pro jakoukoliv jinou funkci. =========== TEST.H ==================== #ifndef _TEST_H_ #define _TEST_H_ class TEST { int data; public: void MInline(int hodnota) { data=hodnota; } void Mswitch(int hodnota) { switch(hodnota) { case 1: data=hodnota; break; default: data=10; } } // Vypíše se varování, že metody se switch nelze přeložit jako inline // Deklarace vně definovaných metod void MExt(int hodnota); void MExtInit(int hodnota=0); inline void MExtInline(int hodnota); inline void MExtInitInline(int hodnota=0); };
// metoda s inicializovaným argumentem // inline metoda // inline metoda s inicializací argumentu
inline void TEST::MExtInline(int hodnota) { data=hodnota; }
// vnější definice
inline void TEST::MExtInitInline(int hodnota) { data=hodnota; }
// 2. výskyt, zde už bez inicializace !
inline float MojeInline(float r=1) { return 6.28*r; }
// normální funkce a její 1. výskyt, inicializace je zde
inline float MojeInline2(float d=2); // deklarace hlavičky - 1. výskyt, inicializace je zde inline float MojeInline2(float d) { return 6.28*d; } #endif
// definice - 2. výskyt, už bez inicializace argumentu
64
=========== TEST.CPP ==================== #include "TEST.H" void TEST::MExt(int hodnota) { data=hodnota; } void TEST::MExtInit(int hodnota) { data=hodnota; } =========== HLAVNI.CPP ==================== #include "TEST.H" // vkládáme jen hlavičky tříd a funkcí void main(void) { TEST test; float f; // Příkaz: test.MExt(1); test.MExtInit(2); test.MExtInit(); test.MInline(3); test.MExtInline(4); test.MExtInitInline(5); test.MExtInitInline(); test.Mswitch(6); f=MojeInline(10); f=MojeInline2();
je-li inline povoleno, pak se příkaz přeloží takto: // test.MExt(1); // test.MExtInit(2); // test.MExtInit(0); - doplněna výchozí hodnota argumentu 0 // test.data=3; // test.data=4; // test.data=5; // test.data=0; - doplněna výchozí hodnota argumentu 0 // test.Mswitch(6); - switch nelze inline // f=6.28*10; // f=6.28*2; - doplněna výchozí hodnota argumentu 2
} U objektů tedy dáváme do header souborů : • deklarace tříd a v nich jejich: - proměnné - definice vnitřních metod - deklarace hlaviček (prototypů) vnějších metod. • definice vnějších inline metod V header souboru se nesmějí nacházet těla vnějších metod, které nejsou tvaru inline. Ty všechny patří do souboru *.cpp. Například, kdybychom v header tam uvedli: void TEST::MExt(int hodnota) { data=hodnota; } linker by ohlásil: „Duplicitní definice TEST::MExt(), v případě, že by se soubor TEST.H vkládal do více překládaných souborů. Inicializace argumentu se vždy uvádí pouze jedenkrát a to při prvním výskytu hlavičky metody, resp. funkce. Pokud toto pravidlo porušíme, nahlásí se chyba: Default argument value redeclared. U všech vnějších definic metod (v header i v CPP souboru) se pomocí :: operátoru scope musí specifikovat před identifikátorem funkce jméno třídy, do níž metoda patří, na znamení, že se doplňujeme definici k deklaraci příslušné třídě. Kdyby se MExt v TEST.CPP zapsala jako: void MExt(int hodnota) { data=hodnota; } tj. bez uvedení TEST:: , pak by se nahlásila chyba, že proměnná data není nedefinovaná. Překladač by zápis MExt chápal jako definici nové funkce, která nemá nic společného se třídou TEST, a proto nedostává ani pointer this.
65
3.2 Přetěžování (overloading) funkcí a metod v C++ C++ rozlišuje funkce a metod nejen podle identifikátorů, ale i podle typů argumentů, se kterými jsou volané. Tato vlastnost se označuje jako přetěžování, overloading. Přetěžování znamená, že existuje několik funkcí se stejným jménem, které pracují s různými typy dat. Překladač z nich vybere vhodnou funkci nebo metodu podle použitých argumentů při jejím volání. ! Podívejme se na trojici funkcí pro tisk čísel: /*1*/ void pis(int i) { printf("%5d",i); } /*2*/ void pis(long l) { printf("%8ld",l); } /*3*/ void pis(int i, char c) { printf("%5d%c",i,c); } které mají stejný název, ale liší se argumenty. Napíšeme-li: pis(5); provede se nám funkce na řádku (1), protože čísla bez označení typu se chápou jako int a překladač C++ bude hledat funkci pis ve tvaru: void pis(int). Zadáme-li však: pis(5L); provede se nám řádek (2), jelikož 5L značí číslo typu long. Obdobně: pis(5, ’%’); vyvolá operaci ze řádku (3), tj. funkci ve tvaru: pis(int, char). Co se však stane, napíšeme-li: pis(5L, ’%’); V našem programu nemáme funkci odpovídajícího tvaru: pis(long, char). Překladač se pokusí najít nejbližší možnou definici. Použije řádek (3) a převede argument 5L z typu long na typ short int. Zadáme-li však příkaz: pis(7.9, 500L); zavolá se sice funkce ze řádku (3), ale současně se nahlásí varování, že se při převodu z čísla long na char dojde ke ztrátě přesnosti. Protikladným případem je: pis("ALFA",’%’); kde se hlásí chyba: „Could not find a match for pis(char *, char)“ = nenalezena odpovídají definice. Převody, které C++ provádí při hledání vhodné funkce, se týkají pouze běžných konverzí čísel. Řetězec se nedá převést na short int. Zajímavý případ se objeví při: pis(5.5); Překladač vypíše chybu: „Ambiguity between pis(int) and pis(long)“, jelikož se nedokáže rozhodnout, na který typ má převést reálné číslo 5.5 - v úvahu přicházejí obě konverze na int i na long. Naproti tomu, příkaz: pis(‘a’); se transformuje bez problémů na volání: pis(int) ze řádku (1), neboť znaky se v C chápou jako typy blízké číslům typu integer. Možnost definovat několik funkcí se stejným názvem se využívá v objektech velmi často. Pomocí ní lze zvýšit univerzálnost použití třídy PRUMER (viz. strana 61) vytvořením několika různých konstruktorů. Příklad: Třída PRUMER s několika konstruktory class PRUMER { double soucet; long pocet; public: PRUMER() { soucet=0; pocet=0; } PRUMER(double soucet0, long pocet0) { soucet=soucet0; pocet=pocet0; } PRUMER(PRUMER & prumer) { soucet=prumer.soucet; pocet=prumer.pocet; } double Prumer() { return pocet>0 ? soucet/pocet : 0; } void Pricti(double cislo) { soucet += cislo; pocet++; } }; První konstruktor vytváří vynulovaný objekt a druhý konstruktor ho inicializuje přednastavenými hodnotami. Třetí konstruktor (tzv. copy constructor) dostává referenci na již vytvořený !
Přetěžování se vztahuje i na operátory, o nichž se bude hovořit na straně 86.
66
objekt a jeho hodnoty okopíruje do vytvářeného objektu. Překladač vybírá konstruktor podle stejných pravidel jaká platí pro metody a funkce. Pomocí argumentů daných objektu při jeho definici lze tedy volit různé metody, jak ho inicializovat, například: PRUMER prum1; // použije se konstruktor PRUMER() PRUMER prum2(100,2); // použije se konstruktor PRUMER(double, long) PRUMER prum3(prum2); // použije se konstruktor PRUMER(PRUMER &) PRUMER prum4(1); // Nahlásí se chyba: "Konstruktor PRUMER(int) neexistuje" Poznámka: Velmi opatrně musíme pracovat s inicializacemi argumentů. Upravíme-li v příkladu funkce pis řádek (3) přidáním inicializace argumentu c na znak '%': /*3*/ void pis( int i, char c ='%' ) { printf( "%5d%c", i, c ); } pak příkazy: pis(5); pis(5L); pis(‘a’); které se dříve přeložily bez potíží, vyvolají chybu: „Ambiguity between pis(int) and pis(int, char)“, neboť překladač neví, jakou deklaraci má použít. V úvahu přichází buď - nejen řádky (1) a (2), nebo také (3), pokud se při volání pis dosadí místo druhého argumentu hodnota '%'.
3.3 Dočasné objekty C++ povoluje pracovat s dočasným (temporary) objektem, který nemá žádný identifikátor a slouží jako pomocná proměnná. Dočasný objekt vytvoříme tak, že napíšeme identifikátor konstruktoru příslušné třídy s potřebnými parametry. Například pro třídu ZASOBNIK ze strany 59 se dočasný objekt zásobníku pro 2000 znaků zadá příkazem: ZASOBNIK(2000); Pro dočasné objekty platí tato pravidla: 1. dočasné objekty přestávají existovat s koncem příkazu, v němž jsou použité; 2. na dočasný objekt můžeme zavolat metodu jeho třídy, např. ZASOBNIK(2000).Push(); 3. v příkazu return lze dočasný objekt použít jedině tehdy, když funkce vrací hodnotu a konstruktor vracené třídy neobsahuje alokace paměti. Například příkaz: PRUMER funkce() { /* nìjaké operace*/ return PRUMER(100,2); }; je v pořádku, protože PRUMER (viz. strana 66) nepoužívá dynamické objekty, ale naproti tomu zápis: ZASOBNIK test() { return ZASOBNIK(100); }; je chybný, protože konstruktor ZASOBNIK(int velikost) alokuje paměť pomocí new[] (viz. strana 59). Funkce test() by nám sice správně vrátila obsah datové struktury třídy ZASOBNIK, ale její ukazatel char * pzasobnik; by směřoval na neexistující paměť automaticky zrušenou operací delete[] v destruktoru ~ZASOBNIK() ihned po skončení příkazu return; 4. použití dočasného objektu v příkazu return u funkce vracející referenci nebo pointer na strukturu je vždy chybné, například: A & funkce() { /* pøíkazy*/; return A(0); } anebo: A * funkce() { /* pøíkazy*/; return & A(0); } vede k narušení paměti - vracíme odkaz na něco, co už neexistuje. Funkce smí vracet výhradně referenci, respektivě pointer, na objekt se scope delším než lokálním, to znamená například: a) na parametr, který jí byl předaný; b) na globální objekt; c) na dynamickou proměnnou, kterou si sama vytvořila. Dočasné objekty jsou významným prvkem, který se používá v objektových knihovnách. Následující příklady si velmi pečlivě prostudujte: #include <stdio.h> class A 67
{ char id; // pomocný identifikátor objektu public: A(char c) { printf("Vznik objektu: %c\n",id=c); } ~A() { printf("Zánik objektu: %c\n",id); } void metoda(void) { printf("Volání objekt_%c.metoda()\n",id); } }; /* Funkce mající třídu A jako parametr */ void funkce1(A a) { printf("Operace funkce1.\n"); a.metoda(); } void funkce2(A & a) { printf("Operace funkce2.\n"); a.metoda(); } void funkce3(A * pa) { printf("Operace funkce3.\n"); pa->metoda(); } /* Funkce vracející třídu A jako parametr */ A funkce4()
{ printf("Operace funkce4.\n"); return A('R'); } /* Vracíme objekt hodnotou, čili předáváme ven z funkce kopii dočasného objektu a to lze. */
A& funkce5(A & a) { printf("Operace funkce5.\n"); return a; } /* Referenci na vstupní argument, či jiný objekt, jehož existence přesahuje trvání těla funkce, lze vracet. Nedá se ale vracet reference na dočasný objekt vytvořený ve funkci. Pří zápisu: A& funkce5() { printf("Operace funkce5.\n"); return A('Y'); } překladač nahlásí chybu. */ A * funkce6()
A * funkce7(A * pa)
void main(void) { A * pa; A a('G'); A('T'); A('M').metoda();
funkce1(A('1'));
funkce2(A('2'));
funkce3(&A('3'));
{ printf("Operace funkce6.\n"); return & A('e'); } /* NEBEZPEČNÁ CHYBA: Vracíme pointer na neexistující objekt. Syntaktická kontrola překladače tento prohřešek nezachytí ! */ { printf("Operace funkce7.\n"); return pa; } /* Pointer na vstupní argument, či jiný objekt, jehož existence přesahuje trvání těla funkce, lze vracet. */ /* V Ý P I S N A O B R A Z O V C E */ // Žádný výpis - objekt nevznikl, je jen pointer na něj // : „Vznik objektu: G“ /* : „Vznik objektu: T“ „Zánik objektu: T“ */ /* : „Vznik objektu: M“ „Volání objekt_M.metoda()“ „Zánik objektu: M“ */ /* : „Vznik objektu: 1“ „Operace funkce 1.“ „Volání objekt_1.metoda()“ „Zánik objektu: 1“ */ /* V Ý P I S N A O B R A Z O V C E */ /* : „Vznik objektu: 2“ „Operace funkce 2.“ „Volání objekt_2.metoda()“ „Zánik objektu: 2“ */ /* : „Vznik objektu: 3“ „Operace funkce 3.“ „Volání objekt_3.metoda()“ „Zánik objektu: 3“ */ 68
(funkce4()).metoda(); /* : „Operace funkce 4.“ „Vznik objektu: R“ „Volání objekt_R.metoda()“ „Zánik objektu: R“ */ { A x('x'); x=funkce4(); x.metoda(); }
// vložení příkazového bloku ------------------------------------------------------// : „Vznik objektu: x“ /* : „Operace funkce 4.“ „Vznik objektu: R“ navíc R je uloženo do x „Zánik objektu: R“ */ // : „Volání objekt_R.metoda()“ // pozn. x==R // : „Zánik objektu: R“ - tj. deklarace A x('x'); -------------------------
(funkce5(A('5'))).metoda(); /* : „Vznik objektu: 5“ „Operace funkce 5.“ „Volání objekt_5.metoda()“ „Zánik objektu: 5“ */ { // vložení příkazového bloku -----------------------------------------------------------A y('y'); // : „Vznik objektu: y“ y=funkce5(A('5')); /* : „Vznik objektu: 5“ „Operace funkce 5.“ // navíc R je uloženo do y „Zánik objektu: 5“ */ y.metoda(); // : „Volání objekt_5.metoda()“ // pozn. y==R } // „Zánik objektu: 5“, tj. deklarace A y('y'); ------------------------------/* Následující příkaz je hrubou programátorskou chybou ! */ (funkce6())->metoda(); /* : „Operace funkce 6.“ „Vznik objektu: e“ „Zánik objektu: e“ ??? „Volání objekt_-.metoda()“ CHYBA !! Pracujeme s uvolněnou pamětí */ /* Následující příkazy jsou opět hrubou programátorskou chybou ! */ pa=funkce6(); /* : „Operace funkce 6.“ „Vznik objektu: e“ „Zánik objektu: e“ */ pa->metoda(); /* : CHYBA !! Pracujeme s uvolněnou pamětí */ /* Dočasný objekt lze bez problémů použít jako argument funkce */ (funkce7(&A('7')))->metoda(); /* : „Vznik objektu: 7“ „Operace funkce 7.“ „Volání objekt_7.metoda()“ „Zánik objektu: 7“ */ /* Následující dva příkazy demonstrují hrubou programátorskou chybu ! */ /* V Ý P I S N A O B R A Z O V C E */ pa=funkce7(&A('7')); /* : „Vznik objektu: 7“ „Operace funkce 7.“ „Zánik objektu: 7“ */ pa->metoda(); /* CHYBA !! Pracujeme s uvolněnou pamětí, protože funkce vrátila pointer na zrušený objekt! */
69
/* Následující příkaz není zcela správný, alokujeme paměť pro objekt, ale už ji neuvolňujeme ani my a ani funkce7() */ ( funkce7(new A('8')) )->metoda(); /* : „Vznik objektu: 8“ „Operace funkce 7.“ „Volání objekt_8.metoda()“ ?? delete */ /* Pro opravu předchozího příkazu využijeme skutečnosti, že funkce7 vrací pointer na vstupní argument */ pa=funkce7(new A('9')); /* : „Vznik objektu: 9“ „Operace funkce 7.“ */ pa->metoda(); // „Volání objekt_9.metoda()“ delete pa; // „Zánik objektu: 9“ } // konec funkce main Důležitá poznámka: Třídy objektových knihoven, zejména grafických, zahrnují někdy v destruktorech delete operace pomocných tříd, které jim uživatel předal při jejich inicializaci. V těchto případech by postup použitý u objektu 8 byl v pořádku (tj. new bez delete), zatímco new s následným delete aplikované u objektu 9 by bylo hrubou chybou. Ve sporných situacích: „Obsahuje destruktor objektu delete pro argument nebo ne?“ bývá jediným spolehlivým vodítkem studium příkladů, které dodávají tvůrci objektových knihoven. Příklady představují mnohem spolehlivější zdroj informací než manuály a nápovědy, protože příkazy programu lze rychle otestovat, což o výrocích napsaných v dokumentaci obecně neplatí.
3.4 Dědění objektů Dědění objektů nám dává možnost rozšiřovat jednotlivé třídy a dokonce i měnit jejich chování. Vezme-li si nejjednodušší případ - naši třídu PRUMER ze strany 66: class PRUMER { double soucet; long pocet; public: PRUMER() { soucet=0; pocet=0; } PRUMER(double soucet0, long pocet0) { soucet=soucet0; pocet=pocet0; } PRUMER(PRUMER & prumer) { soucet=prumer.soucet; pocet=prumer.pocet; } double Prumer() { return pocet>0 ? soucet/pocet : 0; } void Pricti(double cislo) { soucet += cislo; pocet++; } }; Budeme-li potřebovat při výpočtu si občas zapamatovat mezivýsledek, musíme třídu nepatrně upravit: class PRUMER2 { double soucet; long pocet; double pamet; public: PRUMER2() { soucet=0; pocet=0; pamet=0; } PRUMER2(double soucet0, long pocet0) { soucet=soucet0; pocet=pocet0; pamet=Prumer(); } PRUMER2(PRUMER2 & prumer2) { soucet=prumer2.soucet; pocet=prumer2.pocet; pamet=prumer2.pamet; } double Prumer() { return pocet>0 ? soucet/pocet : 0; } void Pricti(double cislo) { soucet += cislo; pocet++; } 70
void ZapamatujSi(void) { pamet = Prumer(); } double PametPrumeru(void) { return pamet; } }; Podobný postup nebuduje program jako stavebnici. Přestože třída PRUMER2 oproti třídě PRUMER používá jen pár prvků navíc, ty jsou v programu zdůrazněné tučným písmem, napsali jsme ji celou znovu. Problém by snad vyřešilo, kdybychom nevytvářeli novou třídu, ale rozšířili původní definici o nové funkce. Uděláme-li ale změny v PRUMER při každém použití, brzy nám nabobtná na mamutí shluk sice univerzálního, ale naprosto nepřehledného kódu. Objektové programování vychází z myšlenky - použít hotovou třídu jako základ a odvodit z ní deklaraci nové třídy specifikováním přidaných prvků. To se nazývá dědičností. Podtržení slova deklarace zdůrazňuje, že dědění se vztahuje výhradně na vnitřní popis. Výchozí třídě se říká základní třída - base class. Odvozená třída z ní přebírá deklarace všech metod a prvků a k nim doplní svoje nové elementy a operace. Odvozená třída je vždy zcela nezávislá entita chovající se stejně tak, jako kdybychom ji celou napsali znova. Rozdíly existují výhradně ve vnitřní reprezentaci, neboť dědění dovoluje sdílet některé metody. Proměnné základní a odvozené třídy jsou však vždy vzájemně separované a od nich vytvořené objekty zabírají disjunktní úseky paměti. " S využitím dědičnosti můžeme zapsat třídu PRUMER2 jako: class PRUMER2 : PRUMER { double pamet; public: PRUMER2() : PRUMER() { pamet=0; } PRUMER2(double soucet0, long pocet0) : PRUMER(soucet0,pocet0) { pamet=Prumer(); } PRUMER2(PRUMER2 & prumer2) : PRUMER(prumer2) { pamet=prumer2.pamet; } void ZapamatujSi(void) { pamet = Prumer(); } double PametPrumeru(void) { return pamet; } }; Řádky zápisu mají následující význam. V hlavičce: "class PRUMER2 : PRUMER" oddělovač : specifikuje, že třída PRUMER2 se dědí (odvozuje) od základní třídy PRUMER. Z té se zdědí veškeré její deklarace, a proto se v PRUMER2 se uvádějí pouze prvky doplněné k základní třídě PRUMER. U proměnných se jedná o jedinou definici a to datový člen pamet. Definice konstruktoru: "PRUMER2() : PRUMER() { pamet=0; }" obsahuje volání konstruktoru PRUMER. Ten se vykoná nejdřív a po něm příkazy k němu přidané, čili inicializace pamet. PRUMER2(double soucet0, long pocet0) : PRUMER(soucet0, pocet0) { pamet=Prumer(); } Obdobné úkony se provedou i v případě druhého konstruktoru s parametry. Zde je novým prvkem pouze volání konstruktoru základní třídy s parametry. Výsledkem bude následující posloupnost příkazů: // volání konstruktoru základní třídy soucet= soucet0; pocet= pocet0; // konstruktor základní třídy // přidané operace v konstruktoru odvozené třídy pamet= pocet>0 ? soucet/pocet : 0; // volaní inline metody Prumer Třetí konstruktor, inicializující třídu pomocí již vytvořeného objektu, používá stejný postup. Jediným náročnějším prvkem je zde předání reference na objekt. PRUMER2(PRUMER2 & prumer2) : PRUMER(prumer2) { pamet=prumer2.pamet; }
"
Disjunktnost datových členů lze obejít deklarací „static“ prvků. Ty zaujímají stejný úsek paměti u každé deklarace třídy téhož typu. (Viz. kapitola 3.10).
71
Konstruktor základní třídy má deklaraci hlavičky PRUMER(PRUMER & prumer); obsahující jako parametr referenci na objekt základní třídy PRUMER. Konstruktoru PRUMER2 se však předává reference na objekt třídy PRUMER2, takže konstruktor základní třídy dostává jako argument objekt jiného typu. Prioritní otázkou je proto vztah objektu základní a odvození třídy. Víme, že objekt tvoří pouze proměnné. Metody existují vždy mimo něj a jako parametr se jim automaticky předává pointer this ukazující na jejich data. Vezmeme-li toto v úvahu, pak objekt prumer2 se od prumer liší pouze v proměnné pamet, kterou prumer2 obsahuje navíc. Vzhledem k tomu, že při dědění se nové deklarace přidávají na konec, mají oba objekty totožné začátky. 8 bytů
PRUMER2 prumer2;
4 byty
double soucet;
long pocet;
8 bytů double pamet;
this
PRUMER prumer;
double soucet;
long pocet;
8 bytů
4 byty
Obr. 3-1 Promìnné základní a odvozené tøídy Jelikož reference představují ve své podstatě automatické pointry, lze referenci na odvozenou třídu. konvertovat na referenci na základní třídu. Samozřejmě totéž platí i pro pointry na třídy. Podobné redukce se používají velmi často, zejména v objektových knihovnách, a proto doporučuji dobře se zamyslet nad uvedeným příkladem. Poslední dva řádky deklarace třídy PRUMER2 představují pouhé definice dvou nových metod, jimiž se rozšiřují možnosti objektu: void ZapamatujSi(void) { pamet = Prumer(); } double PametPrumeru(void) { return pamet; } Poznámka1: Dědit lze i od více tříd. Ukažme si příklad třídy X odvozené od tří tříd Y,Z a Q, což znamená, že v sobě zahrnuje všechny jejich metody a proměnné. class X : Y, Z, Q // zápis dědění od tříd X, Z a Q { int xpromenna; // proměnná třídy X X(int data) : Y(data), Z(1,3), Q() /* konstruktor třídy X volající konstruktory jednotlivých základních tříd X, Z a Q. */ { x= 1; }; // inicializace proměnné patřící třídě X }; Vícenásobné dědičnosti se ale používají ojediněle, viz. dále kapitola 3.7. Častou situací bývá ale řetězení dědičnosti, kdy se od jedné třídy odvodí několik dalších, a od nich zase nové třídy a tak dále. Vzniká tím hierarchický strom tříd - objektová knihovna. Poznámka2: Při výkladu pointrů se diskutovala situace, kdy se identifikátor pole přiřazením redukoval na pouhý datový pointer. Přišel tím tedy o část svých vlastností. Situace se vzdáleně podobá redukci pointru z nadřízené třídy na podřízenou.
72
3.5 Dědičnost a konstruktory a destruktory Destruktory základní třídy se nedědí. Lze je sice volat z bezprostředně odvozené třídy, ale s výjimkou velmi speciálních a málo běžných situací, které vznikají hlavně nestandardním alokováním paměti, bývají podobné operace zbytečné. Z konstruktoru odvozené třídy se zpravidla volá konstruktor základní třídy. Není to sice nezbytně nutné, veškeré inicializace lze někdy provést i v odvozené třídě za předpokladu, že základní třída nemá elementy s atributy private, o nich blíže v další části. Volání konstruktorů probíhá zespodu, tj. od základní třídy k odvozeným. Je-li děděno od více tříd, vyvolávají se konstruktory v pořadí, v němž jsou uvedené v seznamu. Destruktory se evokují v opačném sledu. Pořadí volání ukazuje následující příklad: #include <stdio.h> struct R { R() { printf("Vznik R.\t"); } // konstruktor pouze vypíše svoji identifikaci ~R() { printf("Zánik R.\t"); } // rovněž destruktor nahlásí svoji identifikaci }; struct SR : R // třída SR odvozená od základní třídy R { SR() : R() { printf("Vznik SR.\t"); }; ~SR() { printf("Zánik SR.\t"); } }; struct T { T() { printf("Vznik T.\t"); }; ~T() { printf("Zánik T.\t"); } }; struct VT : T // třída VT odvozená od základní třídy T { VT() : T() { printf("Vznik VT.\t"); }; ~VT() { printf("Zánik VT.\t"); } }; struct U_VT_SR : VT, SR // třída U_VT_SR odvozená od dvou základních tříd VT a SR { U_VT_SR() : VT(), SR() { printf("Vznik U_VT_SR.\n"); }; ~U_VT_SR() { printf("Zánik U_VT_SR.\t"); }; }; void main(void) { U_VT_SR u; // deklarace dat třídy u - volá se konstruktor U_VT_SR() } // s koncem main() zaniká i u - volá se destruktor ~U_VT_SR() Program nám vypíše: Vznik T.
Vznik VT.
Vznik R.
Vznik SR.
Vznik U_VT_SR.
Zánik U_VT_SR.
Zánik SR.
Zánik R.
Zánik VT.
Zánik T.
3.6 Atributy přístupu pro odvozené třídy Zaveďme si, pouze pro účely této kapitoly, pojmy definující způsob přístupu na členy tříd. Mějme dvě třídy: struct A // základní třída { int a; // proměnná a třídy A void ma(); // metoda ma třídy A }; void A::ma(){ a=1; } /* IP - interní přístup - přistupuji na proměnnou či metodu z metody popsané v té samé třídě */ 73
struct B: A { int b; void mb() { b=2; ma(); } }; void fce() { A avar; B bvar; avar.a = 1; bvar.mb(); bvar.ma();
// třída odvozená B děděním od třídy A // proměnná b třídy B // metoda mb třídy B // opět IP - interní přístup /* IPS - interní přístup shora - přistupuji na proměnnou či metodu základní třídy z metody odvozené třídy */ // libovolná funkce nepatřící třídě A a B /* EP - externí přístup, přistupuji přes objekt třídy na metodu či proměnnou deklarovanou v té samé třídě */ // opět EP - externí přístup // EPS - externí přístup shora, přes objekt třídy přistupuji na metodu či proměnnou deklarovanou v základní třídě */
}; Dosud jsme při deklaraci tříd používali pouze dva atributy přístupu public a private, ale C++ zná celkem tři atributy: • private - specifikuje pouze soukromé prvky třídy, na které existuje výhradně IP. • protected - určuje prvky dovolující IP a IPS • public - umožňuje jakékoliv přístupy Tyto atributy můžeme použít i při odvození a pozměnit přístup na členy základní třídy. Při deklaraci tříd A a B jsme uplatnili klíčové slovo struct, které má jako výchozí atribut přístupu public, a proto třída B byla odvozena od třídy A s public atributem. Chceme-li použít jiný atribut, napíšeme ho před jméno základní třídy: struct B : private A {/*...*/}; struct B : protected A {/*...*/}; struct B : public A {/*...*/}; struct B : A {/*...*/};
// // // //
třída B odvozená private děděním od třídy A třída B odvozená protected děděním od třídy A třída B odvozená public děděním od třídy A totéž jako předchozí řádek: struct B : public A {/*...*/};
Použijeme-li klíčové slovo class, bude jediná odlišnost ve výchozím atributu - ten se změní na private. class B : private A {/*...*/}; class B : protected A {/*...*/}; class B : public A {/*...*/}; class B : A {/*...*/};
// // // //
třída B odvozená private děděním od třídy A třída B odvozená protected děděním od třídy A třída B odvozená public děděním od třídy A totéž jako první řádek: class B : private A {/*...*/};
Atribut přístupu použitý při odvození mění atributy členů základní třídy pro IPS a EPS. Existuje celkem devět kombinací, které popisuje následující tabulka: Přístup na členy základní třídy přes základní třídu Atribut přístupu v základní třídě private
IP +
EP -
74
přes odvozenou třídu Atribut odvozeni
IPS
EPS
private
-
-
protected
-
-
public
-
-
protected
public
+
+
-
+
private
+
-
protected
+
-
public
+
-
private
+
-
protected
+
-
public
+
+
+ znamená povolený přístup na člen základní třídy - specifikuje nedovolený přístup na člen základní třídy Na základě tabulky lze stanovit jednoduchá pravidla pro přístup do základní třídy: - členy private jsou vždy nedostupné pro odvozené třídy a externí přístupy, - členy protected a public jsou vždy dostupné pro interní přístupy a interní přístupy shora, - externí přístupy jsou možné pouze na členy public. Odvozená třída se samozřejmě může stát základní třídou pro nové třídy, které se opět odvozují s užitím atributů přístupu, např. class C : public B {/*...*/};
// třída C odvozená protected děděním od třídy B
Pro tento případ se prvky třídy A (základní třídy pro B) chápou jako součást B (základní třídy pro C), ale jejich atribut je změněn podle atributu odvození B od A takto: Atribut přístupu člena základní třídy A private
protected
public
Atribut odvozeni B
Změna atributu člena A v odvozené třídě B na
private
-
protected
-
public
-
private
private
protected
protected
public
protected
private
private
protected
protected
public
public
Pro členy A platí při dědění C od B již probraná pravidla pro přístup na členy základní třídy ve smyslu jejich změněných atributů. Obecně lze pak říct, že použitý atribut přístupu limituje protected a public atributy členů základní třídy na svou úroveň. Bude-li protected, pak výsledný atribut bude private nebo protected, a bude-li private, výsledkem bude private Při rozhodování, který atribut použít, se můžeme opřít o následující pravidla: • atribut private přidělíme těm členům třídy, jenž budou používat výhradně metody té samé třídy (IP), • atribut protected dáme těm členům, u nichž předpokládáme přístupy z odvozených tříd (IPS), • atributem public označíme takové členy, na které smějí být externí přístupy,
75
• třídu B odvodíme od A s atributem private, pokud si přejeme, aby další třídy odvozené od B nesměly používat ze třídy A členy protected a public a současně si přejeme zakázat externí přístupy shora na public členy A, • třídu B odvodíme od A s atributem protected, žádáme-li, aby další třídy odvozené od B mohly pracovat s prvky protected a public třídy A, ale zároveň nebyly dovolené externí přístupy shora na public členy A, • třídu B odvodíme od A s atributem public, chceme-li ke členům public třídy A povolit externí přístupy shora přes B a přes další třídy odvozené od B. Poznámka: Uvedená pravidla lze obejít pomocí deklarace friend - ta bude tématem kapitoly 3.8. Ukažme si dědičnost na příkladu: class X { // atribut přístupu je teď nastaven na „private“ (default pro class) int xpri; int xfpri(void) { return 1; } protected: // chráněné prvky, pro metody třídy X a tříd z ní odvozených int xpro; int xfpro(void) { return 2; } public: // veřejně přístupné prvky int xpub; int xfpub(void) { return 3; } /* Kterákoliv metoda třídy má přístup na všechna data a metody své třídy bez ohledu na atribut přístupu. Ten omezuje výhradně cizí metody a funkce. */ private: void Nuluj1(void) { xpri=xfpri(); xpro=xfpro(); xpub=xfpub(); } protected: void Nuluj2(void) { xpri=xfpro(); xpro=xfpub(); xpub=xfpri(); } public: void Nuluj3(void) { xpri=xfpub(); xpro=xfpri(); xpub=xfpro(); } }; /* Kterákoliv metoda odvozené třídy, bez ohledu na atribut svého odvození, má přístup pouze k datům a metodám základní třídy, jež mají atributy protected nebo public. Prvky základní třídy s atributy private jsou pro ně vždy nepřístupné. */ class YPUBL : public X { metoda() { xpro=xfpro(); xpub=xfpub();} }; class YPROT : protected X { metoda() { xpro=xfpro(); xpub=xfpub();} }; class YPRIV : X { metoda() { xpro=xfpro(); xpub=xfpub();} }; // definice YPRIV je ekvivalentní zápisu: class YPRIV : private class X { /*...*/ }; void funkce(void) // libovolná funkce, nečlen třídy, třeba main() { X x; // přes x mají cizí funkce přístup pouze k public prvkům třídy X x.xpub=0; x.xfpub(); /*!!*/ x.xpro=x.xfpro(); x.xpri=x.xfpri(); // CHYBA: „Není přístupné“. YPUBL ypubl; ypubl.xpub=0; ypubl.xfpub(); /* přes ypubl mají externí funkce mají přístup jen k public prvkům základní třídy X */ /*!!*/ ypubl.xpro=ypubl.xfpro(); ypriv.xpub=ypriv.xfpub(); // CHYBA: „Není přístupné“. YPROT yprot; YPROT ypriv; 76
/*!!*/ yprot.xpub= yprot.xfpub(); ypriv.xpub= ypriv.xfpub(); // CHYBA: „Není přístupné“. /* Přes yprot a ypriv nemají externí funkce přístup k žádným prvkům základní třídy X, protože smějí pracovat výhradně s elementy public. YPROT a YPRIV odvozené s atributy nižšími než public mají pro prvky třídy X atributy přístupu pod touto úrovní. */ }; /* Metody tříd odvozených YPROT a YPUBL mohou pracovat s public a protected prvky třídy X, jež mají v YPROT a YPUBL atributy přístupu vyšší než private - díky odvození tříd Y.... s atributem přístupu k X na úrovni public, resp. protected. */ class ZYPUBL : public YPUBL { void metoda(void) { xpro=xfpro(); xpub=xfpub(); } }; class ZYPROT : public YPROT { void metoda(void) { xpro=xfpro(); xpub=xfpub(); } }; /* Metody tříd, které jsou odvozené od YPRIV, nesmějí pracovat s prvky třídy X, protože metoda odvození private třídy YPRIV od třídy X, snížila u členů public a protected patřících do třídy X jejich přístup ze třídy YPRIV na private. Prvky private základní třídy, pro ZYPRIV je to YPRIV, jsou pro odvozenou třídu, tady ZYPRIV, vždy nedostupné. */ class ZYPRIV : public YPRIV { void metoda(void) { /*!!*/ xpub=xfpub(); } };
// CHYBA: „Není přístupné“.
void JinaFunkce(void) // libovolná funkce, nečlen třídy { ZYPUBL zpubl; /* přes zpubl mají cizí funkce přístup jedině k public prvkům třídy X */ zpubl.xpub=zpubl.xfpub(); ZYPROT zprot; ZYPRIV zpriv; /* přes zprot a zpriv nemají cizí funkce přístup k prvků třídy X, protože je nemají ani přes základní třídy YPROT a YPRIV */ /*!!*/ zpriv.xpub=0; zpriv.xfpub(); zprot.xpub=zprot.xfpub(); // CHYBA: „Není přístupné“. }; Ukažme si dědění ještě na dřívějším příkladu, na třídě ZASOBNIK ze strany 59: #include conio.h #include ctype.h #include stdio.h #include "ZASOBNIK.H" /* NovyZASOBNIK bude odvozen jako public, to znamená, že všechny prvky - public ze ZASOBNIK budou mít dostup public v NovyZASOBNIK - protected ze ZASOBNIK budou mít dostup protected v NovyZASOBNIK - private ze ZASOBNIK jsou vždy nedostupné. */ class NovyZASOBNIK : public ZASOBNIK { int element; // přidáme nový člen, jenž bude private: (default pro class) public: NovyZASOBNIK(int velikost) // definice konstruktoru nové třídy : ZASOBNIK(velikost) // nutné volání konstruktoru základní třídy { element=0; }; // příkazy konstruktoru odvozené třídy void Tisk(char * jmeno); // přidaná metoda nové třídy }; void NovyZASOBNIK ::Tisk(char * jmeno) // definovaná vně kvůli while { char vystup; printf("\n%s element %2d - počet znaků %2d: ", jmeno, ++element, vrchol()); while(0!=(vystup=Pop())) putch(vystup); 77
} int main(void) { char vstup; NovyZASOBNIK cisla(100), necisla(200); if( cisla.JePlny() || necisla.JePlny() ) return 1; while((vstup=getch()) != 27) // čteme znaky až do znaku ESC { if(isdigit(vstup)) cisla.Push(vstup); else necisla.Push(vstup); if(vstup==' ') { cisla.Tisk("Cisla"); necisla.Tisk("Necisla"); } } return 0; // volá se destruktor NovyZASOBNIK a tím i ZASOBNIK }
3.7 Viditelnost proměnných při dědění Tato část se opírá o viditelnost proměnných, rozebranou v úvodu (str. 13), a současně i o pravidla pro přetěžování (overloading - str. 66). Mějme třídu pro tisk čísel, která sama zjišťuje celkový počet vytištěných znaků, k čemuž využijeme faktu, že funkce printf() vrací počet napsaných znaků. Ten budeme připočítávat k proměnné delka. #include <stdio.h> class Tisk { protected: // odvození protected kvůli dědění int delka; void Plus(int pocetznaku) { delka+= pocetznakui; } // přičti počet vytištěných znaků public: Tisk() { delka=0; }; void pis(int i) { Plus(printf("%5d",i)); } // vytiskni číslo a přičti počet jeho znaků void pis(long l) { Plus(printf("%8ld",l)); } }; Zkusme od Tisk odvodit novou třídu, v níž přidáme k metodám tisku také výstup řetězců. class NovyTisk : public Tisk { public: NovyTisk() : Tisk() { }; void pis(char * s) { Plus(printf("%s",s)); } }; Pokud někde v programu naši třídu použijeme, dostaneme: void main(void) { Tisk tisk; NovyTisk ntisk; tisk.pis(1); tisk.pis(2);
/* tisk 1 a 2 pomocí třídy Tisk, překladač vybere vhodnou metodu podle typu argumentu */ ntisk.pis("Hodnota = "); // Tisk textu „Hodnota = “ pomocí třídy ntisk /* !! */ ntisk.pis(10); // CHYBA: „Cannot convert int to char *“ ntisk.Tisk::pis(10); // v pořádku, chybu jsme odstranili použitím operátoru scope 78
} Přejmenujeme-li ve třídě NovyTisk funkci pis na npis: class NovyTisk : public Tisk { public: NovyTisk() : Tisk() { }; void npis(char * s) { Plus(printf("%s",s)); } }; a zaneseme tuto změnu do programu, operátor :: se již nemusí použít: void main(void) { Tisk tisk; NovyTisk ntisk; tisk.pis(1); tisk.pis(2); // tisk 1 a 2 pomocí třídy Tisk ntisk.npis("Hodnota = "); // Tisk textu „Hodnota = “ pomocí třídy ntisk ntisk.pis(10); ntisk.pis(11L); // Tisk čísla 10 a 11 pomocí třídy ntisk } Tato ukázka demonstrovala způsob jakým překladač vybírá jména metod. Pátrá po nich podle následujícího postupu: 1. Napřed se hledá identifikátor metody v deklaraci třídy, přes kterou je metoda použita. Nalezne-li se tam, pak se vybere, běžným postupem pro přetěžování metod, varianta metody vhodná pro typy použitých argumentů. Pokud nevyhovuje žádná varianta metody, nahlásí se chyba. V předchozím příkladu by příkaz: „ntisk.pis(10);“ vyvolal pátrání po identifikátoru pis v definici třídy NovyTisk a jeho variantě pis(int). 2. Není-li identifikátor ve výchozí třídě deklarovaný, pak se prohledávají základní třídy, z nichž byla odvozená. Pátrání probíhá podle pořadí dědičnosti tříd a bez ohledu na viditelnost nebo atributy přístupu. 3. Pokud se identifikátor metody nachází v nějaké třídě, pak se podobně jako v bodě 1 vybere vhodná metoda, tj. metoda nejvíce odpovídající argumentům použitým při volání. Pokud taková není, nahlásí se chyba a nepokračuje se již v prohledávání dalších základních třídách. Vytvořme na ukázku třeba následující třídy: class T4 { public: long moc(long i) { return i*i; } double moc(double d) { return d*d; } }; class T3 : public T4 { public: char moc(char c) { return c*c; } const char * moc(void) { return "moc"; } }; class T2 : public T3 { /* ... */ }; class T1kruh : public T2 { public: float plocha(float r) { return 3.141592 * moc(r); } }; class T1koule : public T2 { public: float plocha(float r) { return 4*3.141592 * moc(r); } }; Popsané definice vytvářejí strom dědičnosti znázorněný na Obr. 3-2. Čísla jednotlivých tříd udávají pořadí hledání identifikátoru moc. Ten se při volání metody plocha, třeba té deklarova79
né v T1kruh, najde ve třídě T3, kde se vybere moc(char), nejbližší metoda volání moc(float). To znamená, že při výpočtu druhé mocniny se použije násobení s přesností na 8 bitů, přestože ve třídě T4 existuje definice druhé mocniny pro čísla double.
T1kruh
T4
Základní třída poslední úrovně
T3
Základní třída 2. úrovně
T2
Základní třída 1. úrovně
T1koule
Výchozí třídy
Obr. 3-2 Strom dìdiènosti Použijeme-li definované třídy v programu, dostaneme: #include <stdio.h> #include void main(void) { double d; d=T1kruh().moc(2.5); printf("%lg\n",d); // špatně 4, použito T3::moc(char) d=T1kruh().T4::moc(2.5); printf("%lg\n",d); // dobře 6.25, moc určeno scope operátorem d=T1kruh().plocha(2.5); printf("%lg\n",d); // špatně 12.5664, použito T3::moc(char) d=T1kruh().plocha(1000); printf("%lg\n",d); // špatně 201.062, přetečení v T3::moc(char) } Předchozí příklad demonstroval několik důležitých věcí: • překrývání identifikátorů může být zdrojem těžko odhalitelných chyb, a proto by se mělo používat pouze v nezbytných případech; • překrytí identifikátoru má absolutní charakter, tj. překrývají se všechny metody se stejným identifikátorem, které existují ve všech nižších třídách stromu dědičnosti. Neprovádí se overloading (přetěžování) mezi metodami na různých úrovních dědičnosti. Ukažme si překrývání identifikátorů ještě na jiném příkladu: #include <stdio.h> class A { public: int ma() { return 1; } int ma(int x) { return 2*x; } int ma(double x) {return 3*x; } int mab(double x) {return 4*x; } int mab(char c) {return 5*c; } }; class B : public A { public: int mab(char x) { return 6*ma(x); } // překrývá obě A::mab() };
80
class C : public B { public: /*!!*/ int x(void) { return ma(10); } /* CHYBA: „Nenalezena metoda ma(int)“. Všechny A::ma(...) jsou překryté metodou C::ma(char *) už teď, přestože deklarace C::ma(char *) se nachází níže, neboť prvky tříd mají scope třídy a nezáleží u nich na pořadí jejich deklarací. Všechny vznikají jako jeden celek. */ int x1(void) { return y1(5.5); } int y(char c) { return mab(c); } // bude se volat B::mab(char) int y(double r) { return mab(r); } // bude se rovněž volat B::mab(char) int y1(double r) { return A::mab(r); } // bude se volat A::mab(double) int ma(char * s) { return s[0]; } /*!!*/ int z(void) { return ma(20.5); } //CHYBA: „Nenalezeno C::ma(double)“ }; void main(void) {C cobjekt; // Vytvoření objektu typu třída C printf("%d ", cobjekt.y('a') ); /* provede se: C::y(char) => B::mab(char) => A::ma(int) výsledkem bude: 6 * (2 * 'a') = 1164 */ printf("%d ", cobjekt.y(314.15) ); /* vykoná se: C::y(double) => B::mab(char) => A::ma(int) výsledkem bude: 6 * (2*(char) ((int) 314.15)) ------ 314 -------------- 116 -----------= 696 ---- ale i =3768, je-li jiný mechanismus předávaní parametrů */ printf("%d ", cobjekt.y1(314.15) ); /* vykoná se: C::y1(double) => A::mab(double) výsledkem bude: (int) (4*314.15) = 1256 */ } Při vícenásobném dědění, tj. odvození jedné třídy od spojení několika základních tříd, jsou pravidla pro překrývání identifikátorů mnohem komplikovanější. Jednotlivé větve stromu dědičnosti se prohledávají jako nezávislé entity. Mějme například následující třídy: class T11 { public: int moc(int i) { return i*i; } double moc(double d) { return d*d; } }; class T10 { /* ... */ }; class T9 { /* ... */ }; class T7 { /* ... */ }; class T5 { /* ... */ }; class T3 { /* ... */ }; class T8 : public T11 { /* ... */ }; class T6 : public T9, public T10 { /* ... */ }; class T2 : public T5, public T6 { /* ... */ }; class T4 : public T7, public T8 { /* ... */ }; class T1 : public T2, public T3, public T4 { public: float plocha(float r) { return 3.141592 * moc(r); } };
81
T9
T5
T10
T11
T6
T2
T7
T3
T8
T4
Základní třídy poslední úrovně
Základní třídy 2. úrovně
Základní třídy 1. úrovně
Výchozí třída
T1
Obr. 3-3 Strom dìdiènosti pøi vícenásobném dìdìní Popsané definice vytvářejí strom dědičnosti znázorněný na Obr. 3-3. Čísla jednotlivých tříd udávají pořadí hledání identifikátoru moc. Ten se najde až v poslední třídě, kde se vybere moc(double), nejbližší volání moc(float). Zkusme použít uvedené třídy ve variantě předchozího programu: #include <stdio.h> void main(void) { double d; d=T1().moc(2.5); printf("%lg\n",d); // 6.25 d=T1().T4::moc(2.5); printf("%lg\n",d); // 6.25, T11::moc leží i ve scope T4 /*!!*/ d=T1().T2::moc(2.5); printf("%lg\n",d); // chyba - T11::moc neleží ve větvi T2 } Kdyby byl však identifikátor moc použitý, kromě v T11, i v nějaké další třídě, ať už pro datový člen nebo pro metodu, pak by výsledek závisel na tom, o kterou větev stromu dědičnosti by se jednalo - budou-li duplicitní moc identifikátory ve stejné větvi stromu či v různých větvích. Například změna deklarace třídy T9 na: class T9 { public: void moc(char * text) { printf("%s",text); }; }; bude mít za následek existenci dvou identifikátorů moc - jeden bude ve větvi T2-T6-T9 a druhý ve větvi T4-T8-T11. Třída T1 je utvořena odvozením od tří tříd - T2, T3 a T4. Prvky těchto tří tříd leží na stejných úrovních, a proto se jejich identifikátory nepřekrývají, jak to bylo vysvětlenou v části o viditelnosti proměnných (str. 13), ale místo toho se nahlásí chyba víceznačných jmen ( Member is ambiguous: T11::moc and T9::moc). Popsaný problém neodstraní ani definování metody jako private, např. class T9 { void moc(char * text) { printf("%s",text); }; // atribut přístupu je nyní private (výchozí) }; Opět se nahlásí chyba víceznačnosti, protože atributy přístupu mají význam pouze pro dostup na metody. Skutečnost, že metoda T9::moc není z T1 dostupná, nemá žádný vliv na algoritmus hledání identifikátoru moc, stejně jako skutečnost, že nová moc používá jiný parametry. Přemístíme-li deklaraci moc ze třídy T9 do T8: class T8 : public T11 { public: 82
void moc(char * text) { printf("%s",text); }; }; pak se duplicita nenahlásí, protože třída T8 se nachází na stejné větvi stromu jako T11. V tomto případě dochází k běžnému překrývání identifikátorů, se všemi dříve uvedenými vlastnostmi, avšak samozřejmě pouze uvnitř T8 - moc definované v T11 se dá zavolat pouze s operátorem scope, neboť překrytí má absolutní charakter. Poznámky: • Vícenásobná dědičnost se pro zmíněné problémy používá ojediněle. Většinou se nahrazuje tím, že objekty potřebných tříd se zařadí mezi členy vytvářené třídy, viz. dále kapitola 3.9. • Překladač hlásí problémy v duplicitě jmen obvykle až při použití sporné metody. Samotná deklarace třídy znamená popis její struktury, který se analyzuje jen z hlediska správné syntaxe. Na chyby v organizaci třídy se přijde, teprve když se s ní začne pracovat.
3.8 Deklarace friend C++ nabízí možnost obejít atributy přístupu pomocí deklarací spřátelených funkcí, friend, metod nebo celých tříd. Spřízněný prvek má potom přístup na všechny členy třídy, chová se jako by patřil mezi metody třídy. Může pracovat i s private prvky. Ukažme si to na příkladu: class TEST; // dopředná (forward) definice tagu třídy, umožňující na třídu odkazovat class A // definice třídy mající všechny prvky private { int Aprivate; int Cteni(TEST & test); void Zapis(TEST & test, int hodnota); }; class B // definice třídy mající všechny prvky private { int Bprivate; int Cteni(TEST & test); void Zapis(TEST & test, int hodnota); }; class TEST // definice třídy mající všechny prvky private { int Tprivate; friend void kolega(TEST & test); // deklarace funkce jako friend třídy TEST friend int A::Cteni(TEST & test); // spřátelená metoda ze třídy A friend class B; // celá třída B jako spřátelená /*!!*/ int VratPriB(B & b) { return b.Bprivate; } /* CHYBA! Spřátelení je jednosměrné, B je přítelem třídy TEST, ale TEST není nijak spřízněno s B, a proto metody třídy TEST nemají přístup na private elementy B. */ }; int A::Cteni(TEST & test) { return test.Tprivate; } /*!!*/ void A::Zapis(TEST & test, int hodnota) { test.Tprivate=hodnota; } /* CHYBA! Ze třídy A je s TEST spřízněna jenom jediná metoda a to Cteni() */ int B::Cteni(TEST & test) { return test.Tprivate; } void B::Zapis(TEST & test, int hodnota) { test.Tprivate=hodnota; } /* Celá třída B je spřízněna se třídou TEST, a proto všechny metody B mají přístup na veškerá data a metody třídy TEST.*/ void kolega(TEST & test) { test.Tprivate=0; } // Definice těla funkce-přítele třídy TEST 83
void main(void) { TEST test;
kolega(test);
/* Vytvoříme objekt test typu TEST. Překladač užije výchozí, default, konstruktor, protože žádný není nedefinovaný. Stejně tak při zániku test využije výchozí destruktor, viz. kapitola 2.52.5b na str. 56 */ // volání funkce kolega() - přítele TEST
} Popisy typu friend narušují hierarchickou stavbu objektů tím, že pronikají k elementům deklarovaným jako nepřístupné pro cizince. Z tohoto důvodu se friend ocitá v postavení skoku goto v modulárním programování. Ten je sice dovolený, protože existují výjimečné situace, kdy se musí využít, ale současně se jeho uplatnění pokládá za nečistý styl, a proto se rezervuje jen pro krajní programátorskou nouzi. Přesně totéž platí i o deklaracích friend. Všimněte si pořadí v jakém byly jednotlivé třídy a funkce deklarované. Členy cizích tříd, na které chceme odkazovat, musejí být napřed popsány, a proto jsme spřízněné metody definovali jako vnější metody (zápisem jejich hlaviček uvnitř třídy) a teprve po dokončení popisu všech tříd jsme doplnili jejich kódy. Kdybychom se pokusili popsat třeba metodu Cteni celou uvnitř třídy A nebo B, nemohli bychom z ní adresovat prvek Tprivate, protože ten by ještě neexistoval. Stejně tak nelze třídu A uvést až po definici třídy TEST, neboť by nešla uvést metoda A::Cteni ve třídě TEST jako friend, jelikož by ještě neexistovala. Hlavičku metody nemůžeme v jazyce C deklarovat mimo její třídu a k jednou provedeným deklaracím obsahu třídy nelze nic nového přidávat. Rozšířit se může výhradně dopředu deklarovaný tag o popis členů třídy (viz. kapitola 1.21.2d1.2d.3 na straně 22). Popis členů třídy se však vždycky provádí vcelku a dřívější deklarace lze použít jen pro odvození nové třídy děděním.
3.9 Objekty a konstanty jako prvky jiných objektů Deklaraci třídy lze uplatnit v programu anebo ji použít pro odvození jiných objektů či může být členem jiné třídy. V posledním případě se pracuje odlišně s jejím konstruktorem. Použijeme-li objekt třídy PRUMER, ze strany 66, jako člen jiné třídy, pak nelze napsat: class NOVATRIDA { const int CCISLO = 100; // CHYBA - inicializace mimo konstruktor PRUMER prumer(100, 10); // CHYBA - inicializace mimo konstruktor int data; NOVATRIDA(int data0) { data = data0; } }; protože se inicializují prvky třídy NOVATRIDA mimo její konstruktor. Příliš nepomůže ani změna programu na: class NOVATRIDA { const int CCISLO; PRUMER prumer; int data; NOVATRIDA(int data0, int soucet0, int pocet0) { data = data0; CCISLO=100; // CHYBA - CCISLO je konstanta! soucet=soucet0; pocet=pocet0; /* CHYBA - prvky soucet a pocet nejsou přístupné, oba mají atributy private */ } }; 84
protože nelze ukládat ani do konstant ani do privátních prvků třídy. Jediným přípustným řešením je uvedení konstruktorů všech objektů tříd a konstant, patřících mezi členy třídy, v seznamu konstruktorů, podobně jako se to provádí při dědění u konstruktorů základních tříd: class NOVATRIDA { const int CCISLO; PRUMER prumer; int data; NOVATRIDA(int data0, int soucet0, int pocet0) : CCISLO(100), prumer(soucet0,pocet0) { data = data0; } }; Všimněte si zápisu konstanty jako objekty a lze psát: int i(5); // const float pi(3.1415926); // char * text("ALFA"); //
CCISLO jako třídy. V C++ se totiž chápou veškeré datové prvky odpovídá int i=5; odpovídá float pi=3.1415926; odpovídá char * text = "ALFA";
Poznámka 1: Ačkoliv syntaxe jazyka C++ připouští inicializace čísel ve tvaru objektů, používá se to ojediněle. Většinou se dává přednost klasické inicializaci s využitím = a objektový zápis se nechává pro konstanty v postavení prvků objektu, kdy je zcela nezbytný. Poznámka 2: Konstanty patřící mezi prvky třídy se zpravidla deklarují jako static - viz. dále.
3.10 Data a metody typu static Někdy potřebujeme, aby všechny instance třídy, tj. definice objektů jednoho typu, sdílely v paměti některé prvky. To umožňuje popis static, jenž může být použit jak pro data třídy, tak pro metody. Mějme deklaraci třídy X a několik objektů jejího typu: X x1, x2; až X xn; které budeme nazývat instancemi objektů třídy X. Potom platí: • datové členy bez popisu static jednotlivých instancí objektů x1 až xn třídy X zaujímají disjunktní místa v paměti; • datové členy deklarované jako static zaujímají totožná místa u všech instancí x1 až xn, tj. upravíme-li jejich hodnotu u jednoho z objektů x1 až xn, změní se u všech; • datové členy static se definují jednou uvnitř třídy X a poté podruhé vně třídy s X:: operátorem viditelnosti; • s datovými členy deklarovanými jako static lze pracovat i bez instance objektu, pokud před jejich identifikátorem uvedeme X:: operátor viditelnosti; • metody třídy X, u kterých není uveden popis static, lze volat z cizích funkcí pouze prostřednictví objektu, například jako: x1.metoda(). Použijeme-li zápis X::metoda() v cizí funkci, třeba v main(), vyhodnotí se to jako chyba; • metody třídy X popsané jako static nedostávají pointer this, a proto se dají volat z cizích funkcí i bez objektu; • static metody třídy X nedostávají pointer this, a proto smějí používat pouze static data třídy a volat static metody. Na ostatní elementy mají dostup pouze prostřednictvím argumentu. Jinými slovy - na rozdíl od ostatních metod, static metody nedostávají automaticky pointer this a ten se jim musí předávat pomocí argumentu v případě, že mají adresovat prvky třídy. Ukažme si to na příkladu: /************** soubor STATIC.H ************************/ #ifndef _STATIC_H_ 85
#define _STATIC_H_ #include <stdio.h> class X { public: int data; static int sdata; int metoda() { return data + sdata; } // metody mají dostup na všechny prvky static int smetoda1() { /* !! */ data=0; // CHYBA - static metody nedostávají pointer this return sdata; // static metody smějí přímo adresovat pouze členy static } static int smetoda2(X & x) { x.data=0; // přes argumenty smějí static metody na prvky svojí třídy return x.data; } }; #endif /************** soubor STATIC.CPP ************************/ #include "static.h" int X::sdata; /************** soubor HLAVNI.CPP ************************/ #include "static.h" void main(void) { X x1,x2; /* !! */ X::metoda(); // CHYBA - nestatické metody lze volat jen přes objekty x1.metoda(); X::smetoda1(); // statické metody lze volat i bez objektu,podobně jako běžné funkce X::smetoda2(x2); /* statickým metodám, které adresují prvky třídy, se musí předávat pointer na daný objekt jako vstupní argument */ x2.data=0; x1.data=100; printf("%d %d \n", x1.data, x2.data); // výsledek: 0 100 x2.sdata=0; x1.sdata=100; printf("%d %d \n", x1.sdata, x2.sdata); // výsledek 100 100, tj. poslední zápis do sdata X::sdata=200;
// se statickými daty lze pracovat i bez objektu
} Poznámka 1: Častým případem použitím static prvků jsou konstanty. Jejich zařazení jako elementů nějakého objektu zvyšuje přehlednost programu oproti situaci, kdy by se uvedly jako globální data, protože tím získají scope objektu. Kromě toho vzhledem k tomu, že hodnoty konstant se pouze čtou, není potřeba, aby každá instance objektu měla jejich kopii, a stačí je pouze sdílet. Právě to dovoluje statická deklarace, která současně umožňuje, aby se s nimi pracovalo i bez objektu. Například v knihovně OWL se základní barvy definují jako statické konstanty univerzálního objektu TColor. Chceme-li použít černou barvu, napíšeme TColor::Black. Poznámka 2: Rozlišujte statické prvky a deklarace uvnitř scope: class X { public: struct COMPLEX { double re; double img; }; 86
static const double E; }; const double X::E = 2.718281828459; // přiřazení hodnoty statické konstantě void main(void) { X::COMPLEX c = { 1, X::E }; // Inicializace: c.re = 1; c.img= 2.718281828459; /*... další operace ..*/ } Oba členy COMPLEX a E lze použít i bez existujícího objektu, ale pouze proměnná E je typu static, tj. má přidělený statický paměťový objekt. COMPLEX představuje deklaraci struktury uvnitř scope X. Poznámka 3: (pro zapomětlivé): Stále mějte na paměti, že klíčového slovo static u globálních objektů udává jejich linkage, viz. strana 23, a v ostatních případech, k nimž patří i příklady v této kapitole, specifikuje duration, viz. strana 16.
3.11 Předefinování operátorů V C++ je možné kromě metod třídy také definovat nové operátory a používat je na objekty dané třídy ve výrazech. Pro předefinování operátorů platí tato pravidla:
1. Lze předefinovat pouze existující operátory. Nelze vytvořit třeba operátor #, protože takový se v C++ výrazech nevyskytuje.
2. Nelze předefinovat operátory : . .* :: : ? 3. Zápisy předefinování operátorů jsou normální funkce, resp. metody, a proto pro ně platí běžná pravidla včetně jejich přetěžování (overloading). Existuje-li více metod operátorů, překladač si sám vybere vhodný podle použitých argumentů a jeho operace vloží i inline, jsou-li pro to splněny podmínky - viz. část o inline metodách.
4. Některé operátory lze předefinovat vně tříd a učinit z nich funkce typu friend a naopak jiné se musejí deklarovat pouze jako metody třídy. Oba způsoby se od sebe liší předáváním argumentů.
5. Máme-li unární operátor, označme ho třeba symbolem @, který je definován jako metoda třídy, pak dostává vstupní argument v ukazateli this a jeho předefinování má tvar: Retval operator @ () kde Retval - je typ, který je výsledkem unární operace.
6. Je-li binární operátor, označme ho opět symbolem @, který je definován jako metoda třídy, pak je TRIDA @ hodnota voláno jako: TRIDA.(operator @ (hodnota)) a metoda operátoru dostává první argument jako pointer this a druhý v hlavičce funkce. Předefinování má tvar: Retval operator @ (Typpar hodnota ) kde Typpar - libovolný typ, který ke třídě přičítáme, Retval - je obvykle typu třída, pro níž operátor definujeme, tj. TRIDA, nebo reference na ni, tj. TRIDA & .
7. Je-li unární operátor @ definován jako vnější funkce, pak dostává vstupní argument v hlavičce funkce a jeho předefinování má tvar: Retval operator @ (Typpar hodnota) kde Retval - je typ, který je výsledkem unární operace Typpar - je obvykle třída či reference na ni
8. Je-li binární operátor @ definován jako vnější funkce, pak dostává oba argumenty v hlavičce funkce a jeho předefinování má tvar: 87
Retval operator @ (Typpar1 h1, Typpar2 h2) kde Retval - libovolný typ, jenž je výsledkem operace Typpar n - libovolné typy, pro něž operaci definujeme, ale jeden z nich obvykle musí být typu třída. Všimněte si posledních podmínek. C++ nám sice dovoluje předefinovat operátory, ale u některých vyžaduje, aby nejméně jeden z parametrů odkazoval na typ třídy. Ukažme si předefinování nejčastějších operátorů na příkladech. 3.11a Operátor [] Operátor [] lze předefinovat výhradně jako metodu třídy. #include <stdio.h> class POLE { int data[10]; // pole data int NULA; // na prvek bude vrácena reference při indexu mimo rozsah public: POLE() { NULA=0; } int & operator [] (int index) // definice operátoru - ten vrací referenci na int { return index>=0 && index<10 ? data[index] : (NULA=0) ; } }; void main(void) { POLE pole; pole[5] = 10; // zápis do pole printf("%d",pole[5]); // čtení prvku pole } Všimněte si vracení parametru typu reference na vybraný prvek. Kdyby výstupem byla jen hodnota int, jako například: int operator [] (int index) { return index>=0 && index<10 ? data[index] : 0; }; potom by takový operator [] umožňoval pouze čtení data, ale už ne jejich ukládání do pole. Použijeme-li však návratový typ int & , pak je nepřípustný příkaz return 0; - referenci na konstantu nelze předat. Z tohoto důvodu se v příkladu používá pomocný prvek NULA. Je-li index mimo rozsah, vrací se odkaz na něj a současně se přitom zápisem (NULA=0) zajišťuje, že prvek má stále hodnotu 0, i když se do něj předtím omylem něco zapsalo. 3.11b
Operátory ++ a --
Oba operátory lze předefinovat jako metody třídy nebo jako vnější spřátelené funkce. Pro rozlišení mezi jejich postfix a prefix verzemi Borland C++ používá pomocný parametr int, v následujícím příkladu označený tučně, který neobsahuje žádnou informaci a slouží výhradně # k odlišení deklarace postfixové verze operátorů ++ a -- . class DEN { int den; public: DEN(int cislo) { den = cislo % 7; } // nutný konstruktor pro return DEN & operator ++ () { ++den; return *this; } // ++ prefix DEN operator ++ (int) { return DEN( den++ ); } // postfix ++ friend DEN & operator -- (DEN & d); // -- prefix friend DEN operator -- (DEN & d, int); // postfix -#
Poznámka: Turbo C++ verze předcházející 2.1 měly implementovanou pouze prefix verzi operátorů ++ a -- .
88
}; DEN & operator -- (DEN & d) { -- d.den; return d; } // -- prefix DEN operator -- (DEN & d, int) { return DEN( d.den -- ); } // postfix -void main(void) { DEN den(0); ++den; // ++ prefix den++; // postfix++ --den; // -- prefix den--; // postfix -} Příklad demonstruje jednu obtíž při používání operátorů. U operací postfix ++ a --, u nichž vracíme výsledek lišící se od obsahu dat předaných v argumentu (přičítáme číslo k int hodnotě den, ale vracíme její předchozí hodnotu typu DEN), musíme definovat vhodný konstruktor a používat ho v příkazu return ke konverzi výsledku. U obou postfixové operací jsme konverzi zařídili vracením výsledku hodnotou, což u proměnné typu DEN to nevadí, neboť celý objekt typu DEN má velikost čísla int. Avšak v případě velké třídy, třeba obsahující tisíc double čísel, by to znamenalo, že by se při každém použití operátoru muselo dočasně umístit tisíc čísel double na zásobník, přičemž se předávání parametrů nedá zde zefektivnit. Reference na objekt lze totiž vracet výhradně jen u prefix verzí operací ++ a – odkazujících na existující objekt. U jejich postfix variant, vyžadujících konverzi výsledku, je použití reference vyloučené, protože není přípustné vracet odkaz na dočasný objekt, viz. část o dočasných objektech na straně 67: /*!!*/ DEN & operator ++ (int) { return DEN( den++ ); } // HRUBÁ CHYBA! U dočasných objektů se též diskutovalo použití new a vracení odkazu na alokovaný objekt: DEN & operator ++ (int) { return *(new DEN( den++ )); } Tento zápis je správný z hlediska syntaxe, ale každé zavolání tohoto operátoru alokuje paměť pro dynamický objekt, který se použije pro předání hodnoty a poté zůstane neuvolněný - ve Win32 do konce běhu procesu a ve Win16 možná až do restartování Windows. U velkých objektů se proto často definují operátory tak, aby měnily jen obsah struktur, na něž jsou aplikované, nebo vracely číselné typy. U operací ++ a -- se z uvedeného důvodu dává přednost jejich prefixovým variantám. 3.11c Operátor = Může být uveden jen jako metoda třídy. Patří k hojně definovaným operátorům a pro strukturu DEN by vypadal takto: class DEN { int den; public: DEN(int cislo) { den = cislo % 7; } DEN & operator = (int cislo) { den=cislo; return *this; } DEN & operator = (const DEN & d) { den=d.den; return *this; } DEN & operator = (const DEN * pd) { den=pd->den; return *this; } }; void main(void) { DEN den1=3, den = 0; den=5; // DEN & operator = (int cislo); den1=den; // DEN & operator = (const DEN & d); den1= & den; // DEN & operator = (const DEN * d); }
89
3.11d Operátory logického porovnání Operátory logického porovnání se přepisují velmi snadno, neboť vracejí typ int. Mohou se definovat jako vnější spřátelené funkce i jako metody. class DEN { int den; public: int operator == (int cislo) { return den==cislo; } int operator == (DEN & d) { return den==d.den; } friend int operator > ( DEN & d1, DEN & d2 ); }; int operator > ( DEN & d1, DEN & d2 ) { return d1.den > d2.den; } 3.11e Operátory >> a << Definují se většinou v jiném významu než jako binární operátory. Zatímco originální >> a << představují binární operace vracející jako svůj výsledek nový objekt, aniž by měnily vstupní objekty, nové definice často provádějí operaci "A >> data" ve smyslu odebrání prvku z objektu A a jeho zapsání do objektu data. Obdobně, opačnou operaci "A << data" definují jako zápis objektu data do objektu A. Tím se odstraňují problémy s předáváním výsledku diskutované u ++ a – operátorů, protože se vrací odkaz na existující objekt A. Operace >> a << tím získávají charakter tak zvaných proudů, stream, které se hlavně využívají při zpracování textů. Popis $ práce se stream leží mimo zaměření tohoto skripta. Jejich použití naznačuje následující příklad třídy RADKA s operátory >> a << pro čtení a zápis řetězce. #include <stdio.h> // printf() #include <string.h> // práce s řetězci strncpy(), strlen() class RADKA { char text[255]; // paměť pro řetězec znaků int index; // index čtení z paměti int konec; // index za poslední zapsaný znak public: RADKA() { index=konec=0; } // konstruktor RADKA & operator = (char * s) // zápis řetězce do paměti { konec = strlen(s); // zjisti délku řetězce if( konec>sizeof(text) ) konec=sizeof(text); // omez délku strncpy(text, s, konec); // kopíruj řetězec s omezením délky index=0; // nastav index čtení na začátek return * this; // vracíme odkaz na třídu } RADKA & operator << (char * s) { int len = strlen(s); if(len+konec>=sizeof(text)) len=sizeof(text)-konec; strncpy(text+konec,s,len); konec += len; return * this; } RADKA & operator >> (char & c)
// připoj řetězec na konec // zjisti délku řetězce // omez délku na velikost paměti // kopíruj řetězec s omezením délky // zvyš délku o novou část // vracíme odkaz na třídu, aby se operace daly řetězit // čti prvek
$
Manuály jazyka C++ věnují použití proudů, stream, značnou pozornost a dokonce mnohem větší, než jejich praktické uplatnění. Proudy měly nahradit printf a scanf funkce, k čemuž nedošlo, protože špatně se v nich formátuje náročnější vstup a výstup a kromě toho produkují neefektivní kód. Zájemci o proudy mohou nalézt jejich vyčerpávající popis v českých knihách o jazyce C - viz. seznam literatury na straně 216.
90
{ c= index> c1 >> c2 >> c3; printf("%c%c%c",c1,c2,c3); } while(c3!=0);
/* Dvojnásobné volání operátoru << na třídu radka. Řazení za sebou je možné, protože operátor << vrací ukazatel na výchozí třídu příkazem: return * this; */ /* Trojnásobné užití operátoru >> na třídu radka, operace se provedou jako posloupnost příkazů: radka >> c1; radka >> c2; radka >> c3; */ // Smyčka vytiskne řetězec: „Alfa Beta Gamma\0\0\0“
}
3.12 Virtuální metody Virtuální metody představují nejdůležitější prvek pro praktické použití dědičnosti tříd. Dovolují totiž měnit chování metod základní třídy, takže při odvozování nové třídy lze základní třídu nejen doplnit o nové vlastnosti, ale rovněž upravit chování některých jejích metod. Význam této skutečnosti se projeví zejména u objektových knihoven. Lze totiž vytvořit jakýsi univerzální výchozí objekt, který realizuje typické chování daného objektu - například nejčastější vzhled tlačítka. Požadujeme-li jiné tlačítko, odvodíme od základního tlačítka novou třídu, v níž se napíší pouze metody, jejichž chování se liší od výchozího vzoru. Pro názornost začneme výklad od třídy pro počítání aritmetického průměru (str. 61), kterou se rozšíříme o možnost výpočtu kvadratické odchylky. To lze i bez použití virtuálních metod. #include <math.h> class PRUMER // základní třída pro výpočet aritmetického průměru { protected: long suma; // součet čísel int pocet; // počet prvků public: void Pricti(int cislo) { suma += cislo; pocet++; } int Prumer(void) { return (int) (suma / pocet); } PRUMER() { suma=0; pocet=0; } }; class KVADR : public PRUMER { double sumkvadr; public: KVADR(): PRUMER() { sumkvadr=0; } void Pricti(int cislo) { PRUMER::Pricti(); // volání funkce základní třídy sumkvadr += (double) cislo * cislo; // přidaná operace } double KvadratickaOdchylka(void) { return sqrt((sumkvadr - suma) / pocet); } }; void main(void) { KVADR kvadr; 91
kvadr.Pricti(1); kvadr.Pricti(2); printf("%lg", kvadr.KvadratickaOdchylka()); }; Rozšíření nečinilo potíže, protože metoda PRUMER::Pricti se v základní třídě nepoužívá. Když se při dědění předefinuje, nová metoda KVADR::Pricti se plně nahradí původní PRUMER::Pricti. (Poznámka: Z kvadr lze volat i původní metodu Pricti pomocí operátoru scope, například kvadr.PRUMER::Pricti(10);) V třídě pro tisk čísel, kterou známe z odstavce o viditelnosti proměnných při dědění, to už tak jednoduché nebude. #include <stdio.h> class Tisk { protected: int delka; // počet vytištěných znaků void Plus(int i) { delka+=i; } // připočti k délce public: Tisk() { delka=0; }; void pis(int cislo) { Plus( printf("%5d", cislo) ); } void pis(long cislo) { Plus( printf("%8d", cislo) ); } }; Metoda Tisk::Plus se volá ze základní třídy a nelze ji pouhým děděním rozšířit tak, aby se tisk zarovnával do sloupců, tj. aby se automaticky odřádkovalo, po překročení zadané šířky tisku: class Sloupec : public Tisk { int sirka; // zadaná šířka sloupce int radka; // počet znaků na řádce protected: void Plus(int i) // nová metoda pro tisk řetězců { Tisk::Plus(i); // volání funkce základní třídy if( ( radka +=i ) > sirka ) { printf("\n"); radka=0; } } public: Sloupec(int n=20) : Tisk() {sirka=n; radka=0; } void spis(char * s) { Plus( printf("%s", s) ); } }; void main(void) { Sloupec sloup(10); sloup.spis("Abcdef"); sloup.spis("ghijkl"); sloup.spis("mnopqr"); sloup.pis(1); sloup.pis(2); sloup.pis(3); } Do sloupce se budou formátovat řetězce zadané metodou spis, protože metoda Sloupec::spis(char *) využívá novou definici Sloupec::Plus(int), ale vše vytištěné pomocí pis bude v jedné řádce, neboť obě metody Tisk::pis(int) a Tisk::pis(long) stále volají Tisk::Plus(int). Jak tato volání přesměrovat na Sloupec::Plus()? Přesměrování umožňuje deklarace metod základní třídy jako virtual. Bude-li virtual použité u metod Tisk::Plus, pak obě metody pis třídy Tisk budou používat Sloupec::Plus() a předchozí program main() bude pracovat podle našich představ. #include <stdio.h> class Tisk { protected: int delka; virtual void Plus(int i) { delka+=i; } public:
// počet vytištěných znaků // virtuální metoda
92
Tisk() { delka=0; }; void pis(int cislo) { Plus( printf("%5d", cislo) ); } void pis(long cislo) { Plus( printf("%8d", cislo) ); } }; class Sloupec : public Tisk { int sirka; // zadaná šířka sloupce int radka; // počet znaků na řádce protected: virtual void Plus(int i) // nová metoda nahrazující Tisk::Plus(int) { Tisk::Plus(i); /* volání původní funkce základní třídy - toto volání nebude díky scope operátoru Tisk:: přesměrováno */ if( ( radka +=i ) > sirka ) { printf("\n"); radka=0; }; } public: Sloupec(int n=20) : Tisk() { sirka=n; radka=0; }; void spis(char * s) { Plus( printf("%s", s) ); } }; Klíčovým slovem virtual stačí označit metodu základní třídy, tj. Tisk::Plus(int). Metody, které ji budou v odvozených třídách nahrazovat, se stanou virtuální bez ohledu na specifikaci. V našem případě je slovo virtual u metody Sloupec::Plus() navíc z hlediska syntaxe, ale má tam význam z důvodu přehlednosti kódu, neboť zdůrazňuje, že se jedná o virtuální funkci. Program tiskne již správně. Všechny instance třídy Sloupec používají novou funkcí Plus. Chování původní třídy Tisk se samozřejmě nezmění a její instance budou stále používat původní metodu Tisk::Plus, protože virtuální přesměrování se deklaruje při odvození třídy Sloupec a má proto význam pouze pro přístup přes její instance. void main(void) { Sloupec sloup(10); // metody pis a spis používají Sloupec::Plus sloup.spis("Abcdef"); sloup.spis("ghijkl"); sloup.spis("mnopqr"); sloup.pis(1); sloup.pis(2); sloup.pis(3); Tisk tisk; // metody pis používají Tisk::Plus tisk.pis(1); tisk.pis(2); tisk.pis(3); }; /* Kontrolní otázka (nevztahující se k virtuálním metodám): Co by se stalo při přejmenování Sloupec::spis(char * s) na Sloupec::pis(char * s) ? */ Pro porozumění rozdílu mezi normálními a virtuálními funkcemi naznačme mechanismus jejich překladu na jednoduchém příkladu. Mějme: class AA { int data1; int data2; public: AA() { data1=1; data2=2; } int metoda1(void) { return 1;} virtual int metoda2() { return 2; } // virtuální metoda void metoda3() { data1=metoda1(); data2=metoda2(); } }; za jistých předpokladů, silně závisejících od parametrů překladače, může být třída AA přelože % ná, jako kdyby se napsal následující kód: %
Existuje několik různých metod překladu virtuálních funkcí. Všechny se opírají o ukazatele na funkce, avšak liší se umístěním těchto ukazatelů. Ty mohou být buď prvky třídy anebo naopak nezávislé tabulky. Pro každou
93
class AA { int data1; int data2; public: AA() { data1=1; data2=2; _metoda2_ptr = metoda2; } // konstrukce objektu int metoda1(void) { return 1; } int metoda2(); // hlavička metody int (* _metoda2_ptr ) (void); /* deklarace proměnné _metoda2_ptr typu pointer na funkci, která vrací číslo typu int a má jeden argument void. */ void metoda3() { data1=metoda1(); data2 = (*_metoda2_ptr) (); // volání funkce, na kterou ukazuje pointer _metoda2_ptr } }; int AA::metoda2() { return 2; }
// kód metody, virtuální metody se nepřekládají jako inline
Při konstrukci instance třídy AA, například při definici objektu AA aa; se vyplní hodnota _metoda2_ptr, tak aby ukazovala na adresu metody AA::metoda2() : Prvek objektu AA aa;
Hodnota
Typ dat
AA::data1
1
int
AA::data2
2
int
AA::_metoda2_ptr
AA::metoda2
int (*) (void)
// pointer na funkci
Když bude od třídy AA odvozena nová třída BB zápisem: class BB : public AA { int data3; public: BB() : AA() { data3=3; } int metoda2(); /* metoda nahrazující virtuální metodu je vždy virtuální, i když vynecháme klíčové slovo virtual v její specifikaci */ }; int BB::metoda2() { return 20; } /* virtuální metody se nepřekládají jako inline */ potom se při konstrukci instance třídy BB, například při definici objektu BB bb; , vyplní hodnota AA::_metoda2_ptr tak, aby pointer ukazoval na metodu BB::metoda2(), čímž se změní i činnost AA::metoda3() základní třídy. Prvek objektu BB bb;
Hodnota
Typ dat
AA::data1
1
int
AA::data2
2
int
AA::_metoda2_ptr
BB::metoda2
int (*) (void) // pointer na funkci
BB::data3
3
int
Samozřejmě ve všech objektech třídy AA, tj. například v již zmíněném objektu AA aa; , má pointer AA::_metoda2_ptr stále hodnotu AA::metoda2(). K jeho přesměrování dochází jen v instancích objektů BB. Popis virtual můžeme tedy považovat za jakousi vyhybku, která dovoluje přesměrovat volání virtuálních metod základní třídy z původních definic na nové. Dostupné zůstávají oba kódy instanci třídy se buď vytváří jejich kopie anebo se sdílejí. Požadovaná metoda překladu virtuálních funkcí se zadává nastavením parametrů překladače.
94
metod, původní i nový, protože se mění výhradně cíl volání. Původní metodu lze adresovat pomocí operátoru scope, v našem případě AA::metoda2(). Není-li uvedený operátor scope, pak se všechna volání metoda2, ze všech metod objektu BB a jeho základních tříd, přesměrují na BB::metoda2. Poznámka 1: Můžeme u třídy mít virtuální destruktor, ale nikoliv virtuální konstruktor. Poznámka 2: Mohlo by se zdá výhodné automaticky definovat všechny metody jako virtuální, aby se daly měnit, jazyk Java to například tak dělá, ale takový přístup znamená, že každé použití metody se provádí přes dereferenci pointeru na funkci, což samozřejmě zpomaluje běh programu a zvětšuje délku kódu o nezbytné tabulky virtuálních metod. Z tohoto důvodu je vhodnější používat virtuální metody s mírou. Poznámka 3: Lze učinit celou základní třídu jako virtuální, což má význam, potřebujeme-li dědit od několika tříd, které mají společný základ. Například napíšeme-li: class A { public: int data; }; class B : public A {}; class C : public A {}; class D : public C, private B {}; void main(void) { D d; /* !! */ d.data = 0; /* CHYBA: „Member A::data and A::data are ambiguous.“ = nelze rozhodnout, která data použít, neboť třída D obsahuje dvakrát deklarace členů třídy A */ } Přidáme-li ale ke způsobu odvození klíčové slovo virtual: class A { public: int data; }; class B : public virtual A {}; class C : public virtual A {}; class D : public C, private B {}; void main(void) { D d; d.data = 0; // V pořádku, ve třídě D jsou teď deklarace třídy A pouze jednou. } Poznámka 4: O virtuálních základních třídách platí dvojnásobnou měrou všechno řečené o virtuálních metodách - výrazně snižují efektivitu kódu, protože veškeré prvky základní virtuální třídy se musí adresovat přes tabulky ukazatelů.
3.13 Template Některé objekty se liší pouze datových typem použitých elementů. Například třída definující pole čísel s kontrolou mezí indexů se dá napsat jako jednoduchý objekt s deklarovaným operátorem []. Mějme dvě takové třídy pro dva různé typy polí, jedno s prvky int a druhé s prvky double, které se budou lišit pouze v použitém typu dat. Nazveme je třeba POLESI a POLED: #include "mem.h" #include "stdio.h" class POLESI { short int * pdata; short int NULL_ITEM; unsigned int max;
95
public: POLESI(unsigned velikost) { if( (long) velikost*sizeof(short int)>0xFFFFl ) // omezení velikosti velikost = 0xFFFF/sizeof(short int); pdata = new short int[velikost]; max = pdata==NULL ? 0 : velikost; } ~POLESI() { if(pdata==NULL) delete pdata; } short int & operator [] (unsigned int index) { return index<max ? pdata[index] : (NULL_ITEM=0); } }; class POLED { double * pdata; double NULL_ITEM; unsigned int max; public: POLED(unsigned velikost) { if( (long) velikost*sizeof(double)>0xFFFFl ) // omezení velikosti velikost = 0xFFFF/sizeof(double); pdata = new double[velikost]; max = pdata==NULL ? 0 : velikost; } ~POLED() { if(pdata==NULL) delete pdata; } double & operator [] (unsigned int index) { return index<max ? pdata[index] : NULL_ITEM=0; } }; Jazyk C++ dovoluje zjednodušit tvorbu podobných kódů užitím template, šablon. Pod těmi si lze představit předpis pro vygenerování kódu. Template dovoluje jediný druh parametrů a to datové typy. Na rozdíl od běžných deklarací, překladač nepřekládá template v okamžiku, kdy narazí ve zdrojovém kódu na jejich popis, pouze si o něm založí informaci do své tabulky. Teprve když se někde vyskytne odkaz na prvek vytvořený pomocí template, překladač si sám vygeneruje deklarace příslušných objektů, ty přeloží a použije. Template lze tedy chápat jako návod k vytvoření deklarace. Syntaxe template není těžká - před definici třídy se uvede hlavička: template kde JMENO je volitelný parametr, a zamění se variabilní datový typ, který má být parametrem template, za JMENO. Pro výše uvedené pole by template vypadal takto: template class POLE { TMP * pdata; TMP NULL_ITEM; unsigned int max; public: POLE(unsigned velikost) { if( (long) velikost*sizeof(TMP)>0xFFFFl ) velikost = 0xFFFF/sizeof(TMP); pdata = new TMP[velikost]; max = pdata==NULL ? 0 : velikost; } ~POLE() { if(pdata==NULL) delete pdata; } TMP & operator [] (unsigned int index) { return index<max ? pdata[index] : NULL_ITEM=0; } }; 96
Při použití template se uvede požadovaný typ dat mezi < > jako parametr: void main(void) { POLE<short int> tsipole(100); // vytvoření objektu tsipole pro uložení čísel short int POLE<double> tdpole(100); // vytvoření objektu tdpole pro uložení čísel double int i; for(i=-10; i<200; i++) // POLE kontroluje meze a index mimo rozsah nenaruší paměť { tsipole[i]=i; tdpole[i]=i*i; } } Oba objekty vzniklé pomocí template se chovají jako normální objekty. Jedinou výjimkou, která práci s template komplikuje, je ladění programů. Metody objektů vzniklých z template nelze krokovat, protože k nim neexistuje zdrojový kód. Ten byl vygenerovaný pouze při kompilaci programu a po ní opět zrušený. Template se proto hodí především pro dobře odladěné objekty. Další nevýhodou template je jejich omezené použití. POLE lze použít pro různé datové typy, ale ne všechny, nepodaří se vytvořit třeba pole pointrů: POLE tstringpole(100); // Chyba To vyžaduje vytvoření nového template. Předpis pro vytvoření kódu z template totiž pracuje zcela neinteligentně, na principu pouhé substituce nahrazující parametr typu hodnotou. Deklarace operátoru [] vrací referenci na objekt a použijeme-li v argumentu POLE pointer, vznikne při generaci syntakticky nesprávný zápis: char * & operator [] (unsigned int index); // Chyba Template lze vytvořit i pro funkce, například: template T tmax(T x, T y) { return (x > y) ? x : y; }; popisuje funkci tmax, jejíž kód se vygeneruje až v okamžiku jejího použití. Zápis v hlavičce funkce představuje pouze syntaktickou formulaci. Aktuálním argumentem nemusí být class, ale lze použít jakýkoliv jiný typ, viz. konec kapitoly 3.9. Zápis funkce tmax ve tvaru template zjednodušuje psaní programu. Není nutné vytvořit dopředu celou skupinu funkcí tmax pro různé typy argumentů (long, double, float apod.).Příslušné funkce vzniknou až v okamžiku jejich použití. Napíšeme-li někde v programu: tmax(100,200); překladač si automaticky vygeneruje a přeloží kód funkce: short int max(short int x, short int y) { return (x > y) ? x : y; }; Generování se samozřejmě provádí pouze jednou a další odkazy na tmax(int,int), používají poprvé zkompilovanou funkci. Poznámka 1: Podmínky překladu (compiler options) zahrnují i umístění templete kódů v případě projektů složených z více souborů. Pokud linker hlásí duplicity symbolů template objektů, zkuste změnit nastavení těchto podmínek. Poznámka 2: Překladače jazyka C++ obvykle zahrnují celé knihovny template (označované jako class library). Jejich podrobný popis by si vyžádal samostatnou publikaci a nelze ho zahrnout do omezeného rozsahu skripta. Doporučuji však čtenáři aspoň povšechné seznámení s nabídkou template těchto knihoven. Většinou bývá dodáván i jejich zdrojový kód, který může sloužit pro inspiraci. Obvykle není nutné pochopit vnitřní strukturu objektů vygenerovaných z template a stačí pouze rozumět jejich použití. Bohužel, i to bývá někdy nemalým problémem. Poznámka 3: V objektovém programování pro Windows se template používají především pro OLE 2. O něm se kvůli limitu místa nebude ve skriptu hovořit, s výjimkou krátké charakteristiky na konci v kapitole 10.
97
3.14 Objekty a implicitní znalosti Předpokládejme, že chceme vytvořit objekt pro znázornění mapy logické funkce čtyř proměnných y=f(a,b,c,d) ve tvaru: 00 01 11 10 ab 00
1
x
0
1
01
1
x
0
1
11
0
0
0
0
10
1
1
0
x
cd Jelikož ovládáme objekty, napíšeme program pomocí nich. Tož, dejme se do toho! Tabulka se skládá z buněk, čar a popisů kolem ní, a proto začneme od nich: class BUNKA { /*.... */ }; class CARA { /*.... */ }; class POPIS { /*.... */ }; z nich pak sestavíme objekt tabulka class TABULKA { BUNKA bunka[16]; CARA svisla[5]; CARA vodorovna[5]; POPIS vodorovny; POPIS svisly; }; Teď napíšeme příslušné konstruktory a metody - ale moment! Chtěli jsme si vytvořit jen zobrazení mapy logické funkce a místo ní vznikají mraky objektů. Tady není něco v pořádku! Zkusme se proto zamyslet nad stylem popisu objektů. Objekty lze popsat dvěma základními postupy - implicitně a explicitně. Implicitní popis znamená vyjádření objektu ve tvaru vzorce; tak je například definována kružnice - předpisem, že její body mají stejnou vzdálenost od středu. Naproti tomu explicitní popis odpovídá výčtu vhodných prvků - explicitní vyjádření kružnice reprezentuje seznam bodů, které na ni leží. Každý popis, explicitní i implicitní, má svoje výhody a nevýhody. Implicitní popis dovoluje vyjádřit údaje v komprimované formě - ve tvaru vzorečku. Ten však nemusí být vždy známý nebo může být příliš složitý pro manipulaci- například některé nelineární rovnice sice dovolují popsat chování systému, ale tady jejich použitelnost končí. Explicitní popis definuje objekt formou seznamu, který lze snadno prohledávat. Na druhou stranu, i u jednoduchých úloh může obsahovat takové množství prvků, že čas nutný pro náročnější manipulace s ním, naroste do astronomických hodnot. Objekty nabízejí možnost explicitního i implicitního popisu. Data objektu představují explicitní prvky, udávající výčet vlastností objektu, zatímco metody objektu se blíží implicitnímu vyjádření. Otázkou zůstává, kolik informace o objektu máme zahrnout do datových členů a kolik do metod. Na začátku kapitoly jsme snažili mapu logické funkce vyjádřit explicitně, což vedlo na enormní nárůst prvků. Zkusme ji napsat implicitně - jako jednoduchý objekt obsahující vstupy a jedinou metodu, která tabulku nakreslí vhodným grafickým algoritmem: class TABULKA { public: int cislo[16]; void Nakresli() { /*....*/ } }; 98
Metodu Nakresli lze napsat snadno - ta narýsuje jen mřížku, kterou vyplní daty a opatří popisky. Komplikace nastanou, bude-li potřeba jiný vzhled, například mřížka nakreslená silnější čárou. Nezbude nic jiného, než sílu čáry přidat jako volitelný parametr metody Nakresli, vytvořit odpovídající datovou proměnnou silacary a tu zahrnout do konstruktoru: class TABULKA { public: int cislo[16]; int silacary; TABULKA(int silacary0) { silacary = silacary0; } void Nakresli() { /*....*/ } }; Jenomže, co když bude potřeba občas i mřížka nakreslená čerchovanými čarami? Pochopitelně i typ čáry se přece dá přidat k volitelným parametrům. Pro ještě větší zvýšení flexibility třídy TABULKA přidáme ještě barvy čar a znaků, použitý font, styl popisu, 3D charakteristiky, animace, zvukové efekty, sběr dat ze sítě.... Nakonec získáme mamutí objekt s tisícem vstupních podmínek. Podobnou cestou se například ubírá Visual Basic, jehož objekty disponují záplavou vlastností, mezi nimiž najít tu pravou si žádá astronomický čas. Ale to je přece vlastnost explicitních popisů! Je to tak, data nahrazuje výčet volitelných parametrů. Třetí cestu popisu objektu nabízejí virtuální metody. Ty se volají z metody Nakresli a provádějí rýsování jednotlivých čár, psaní a popisků. Každá z nich realizuje nejčastější vzhled prvku. Pomocí nich lze třídy TABULKA napsat: class TABULKA { public: int cislo[16]; void Nakresli() // kreslení tabulky { /*....*/ Cara(...); /*....*/ Udaj(...); /*....*/ Text(...); } // Virtuální metody volané z Nakresli virtual void Cara( ... ); // Nakreslení čáry virtual void Udaj( ... ); // Vyplnění políčka virtual void Text( ... ); // Nakreslení popisu }; Bude-li třeba změnit vzhled čáry, stačí odvodit nový objekt a příslušnou virtuální metodu nahradit jinou. class MOJETABULKA { public: virtual void Cara( ... ) { /*...Moje čára */ }; // Kreslení vlastní čáry }; Virtuální metody vyžadují větší úsilí pro realizaci změny - je nutné psát program - ale zato dovolují prakticky neomezené variace vzhledu čáry a dalších prvků. Postup, který si pro realizaci objektu zvolíme, závisí jen na našem uvážení. Návrh objektu znamená zejména hledání vhodného kompromisu mezi explicitním, implicitním a virtuálním popisem. Shrneme-li předchozí úvahy, máme na výběr tři různé metody, z nichž každá má svoje přednosti a nevýhody, a proto se zpravidla kombinují všechny: • Kostky stavebnice - Vytvoříme mnoho užitečných objektů, ze kterých půjde rychle vybudovat všechno možné. Škoda jen, že v naší stavebnici, ač ji pořád pilně doplňujeme, postrádáme právě ty prvky, které zrovna potřebujeme. Zato nám soustavně přebývá spousta kostek, jež se jako na potvoru nikam nehodí.
99
• Ovládací prvky - Napíšeme několik univerzálních objektů pro nejčastější operace a každý vybavíme volitelnými parametry, jejichž nastavením lze řídit jeho výsledné chování. Naše megabytové super dokonalé objekty umějí naprosto vše. Stačí pro ně mít dost místa na disku a v nějakém konečném čase t stop aproximovat hodnoty jejich parametrů tak, aby se výsledné chování objektu aspoň částečně přiblížilo našim představám. • Výměnné moduly - Všechny operace, u nichž se předpokládá možnost změny, realizujeme jako virtuální metody. Ty se pak dají vyměnit za jiné pomocí dědění. Vše, co vám nevyhovuje, si laskavě napište sami!
100
4 Architektura programů pro Windows Windows přinesly nejen změnu stylu programování oproti DOSu, ale také řadu nových pojmů. Začneme proto krátkou terminologickou poznámkou a chronologií vzniku Windows, neboť z ní vyplývá řada vlastností tohoto operačního systému. Poté přejdeme k vysvětlení hlavních pojmů důležitých pro tvorbu programů.
4.1 Více-... Dnes se často skloňují pojmy obsahující předponu více- jako víceprocesní, víceúlohový, víceprocesorový, vícevláknový a víceuživatelský. Následující části uvádí základních pojmy z operačních systémů (OS) a vysvětlení jejich přesného významu: Aplikace - angl. application - ve Windows označuje soubor typu *.EXE, který zahrnuje kód programu spolu s globálními a statickými proměnnými a dále pomocné informace pro operační systém. Navíc může obsahovat i datové zdroje (resources) jako bitové mapy, ikony, tvary dialogů a menu. Pozn. Spojení všech částí aplikace do jednoho souboru je specifikum Windows (převzaté z počítačů Macintosh). Aplikace pro jiné OS mohou být reprezentované několika různými soubory a samozřejmě mít i jiné přípony než EXE. Proces - angl. process -znamená kód programu nahraný do paměti ze souboru aplikace spolu s globálními a statickými proměnnými. Proces má svůj deskriptor procesu vytvořený OS, pomocí něhož s ním OS manipuluje, a patří mu jeho zdroje jako například otevřené soubory, vytvořená okna, dialogy, datové oblasti a dynamicky alokované části paměti. Všechny tyto prvky zanikají spolu s ukončením procesu. Úloha - angl. task - zpravidla označuje několik spolupracujících procesů řešících jednu úlohu. V literatuře o Win16 se procesy často nesprávně nazývají úlohami, aby se zdůraznilo, že mohou navzájem sdílet některé datové zdroje. Ve Win32 je sdílení datových zdrojů omezeno, a proto se důsledněji hovoří o procesech. Vlákno - angl. thread nebo ligh-weight-process - představuje posloupnost (řetězec), ve kterém se postupně provádějí jednotlivé instrukce. Každý proces má nejméně jedno vlákno, označované jako hlavní vlákno (main thread), jinak by nemohl vykonávat žádné operace. V prostředí DOS a Win16 mají všechny procesy právě jedno vlákno. Přerušení - angl. interrupt - znamená přerušení právě prováděného vlákna a vložení jiné posloupnosti operací - tzv. přerušovacího programu. Ten dostává silně limitovaný přístup ke službám systému a může provádět pouze omezenou množinu operací, aby nenarušil běžící procesy. Rezidentní program - angl. Terminate and Stay Resident (TSR) - představuje program, který trvale zůstává v paměti počítače, avšak nemá svoje vlákno. Spouští se na omezenou dobu pomocí operace přerušení, a proto podléhá jeho omezením. Driver - angl. driver - představuje podprogram, který přistupuje přímo na hardware počítače. OS mu povoluje jiná oprávnění než běžným procesům, ale na druhou stranu na něj klade řadu omezení pro manipulaci se systémovými zdroji. Obecně možno říct, že bez driverů proces nemůže jednoduše pracovat s periferiemi systému. Psaní driverů vyžaduje velmi zkušené profesionály a obtížnost jejich vytvoření pro OS řady Windows prudce stoupá s rostoucí bezpečností systému. Víceprocesní (resp. víceúlohový) operační systém - angl. multitask operating system - dokáže spustit několik procesů najednou a každému z nich obhospodařuje jeho vlastní zdroje. Na počítači, který má jediný procesor, je víceprocesní běh pouhou iluzí, dosaženou přepínáním mezi jednotlivými procesy. Velmi zjednodušeně lze říct, že každý proces dostává přidělené drobné úseky času procesoru (time slices), jejichž délku a četnost řídí OS podle priority 101
přidělené procesu a podle okamžitého stavu procesu - neaktivní procesy dostávají zpravidla méně času a procesy čekající na vstup mohou být úplně pozastavené. Událostmi řízený víceprocesní systém - event driven multitask - přepíná jednotlivé procesy pouze při výskytu některých událostí, jako například během volání vhodné funkce. Tak se to děje např. ve Win16. Preemptivní víceprocesní systém - pre-emptive multitasking - dokáže přepínat úlohy, kdykoliv to potřebuje (pre-emption znamená v angličtině přednostní právo). Windows NT představují čistě preemptivní víceprocesní systém. Windows 95 a 98 spouštějí 32-bitové aplikace v preemptivním módu, ale 16-bitové ve módu Win16. Více vláken - multi-threaded process - V rámci jednoho procesu může existovat několik samostatných vláken, což si lze představit, jako by se vykonávalo několik vývojových diagramů najednou. Na rozdíl od procesu není přepínání mezi vlákny řízeno přímo jádrem OS, ale speciální knihovnou, která „pořádá„ přepínání kontextu mezi vlákny, aniž by se do hry zatahovalo bezprostředně jádro OS. Z hlediska jádra OS je pak vícevláknový proces chápán jako jediný standardní proces. Vícevláknový běh uvnitř procesu samozřejmě nezrychlí čistě výpočetní úlohy, avšak například umožňuje vstupní a výstupní operace provádět na pozadí. Více vláken podporují všechny víceprocesní systémy schopné pracovat v preemptivním módu, třeba Win32. Víceprocesorový operační systém - angl. multiprocessor operating system - obhospodařuje několik procesorů a všechny využívá pro řešení úloh. Rozlišuje se asymetrický víceprocesorový systém (asymmetric multiprocessor system ASMP), v němž operační systém běží pouze na jednom procesoru, který současně zpracovává všechny vstupy a výstupy, a ostatní procesory s nimi komunikují jeho prostřednictvím. Jeho opakem je symetrický víceprocesorový systém (symmetric multiprocessor system SMP), ve kterém operační systém může běžet na kterémkoliv procesoru, nebo dokonce na více z nich současně. Windows-NT jsou příkladem preemptivního víceprocesního SMP systému. Víceuživatelský systém - multiuser system - obsahuje podporu pro ochranu jednotlivých uživatelů zpravidla na úrovni přístupových práv k souborům a jejich individuálních nastavení. Patří sem tedy nejen všechny UNIXy, ale např. i Novell, Win-NT a částečně i Win95, jsou-li tak nakonfigurovány. Aplikace, proces, úloha, program, vlákno - v literatuře o Windows se většinou nerozlišuje důsledně mezi procesem a aplikací. Často se hovoří o aplikaci a myslí se tím z ní vytvořený proces, resp. úloha. Rovněž tak se běžně používá slovo program jako univerzální označení pro posloupnost prováděných operací, čili pro vlákno. Podobné směšování pojmů vzniklo v dobách, kdy počítače byly schopné provádět jediný program, a vzhledem k tomu, že se podobné záměny se staly v technické literatuře běžné, přidržíme se jich i v této publikaci. Důsledné rozlišování mezi aplikací, procesem, úlohou a vláknem by čtenáře jen mátlo, protože by nesouhlasilo s terminologií jiné literatury. Aplikací nebo programem proto budeme nazývat, shodně s většinou ostatních publikací, běžící proces, resp.úlohu, a budeme-li se hovořit o souboru aplikace, bude to patřičně zdůrazněno s odkazem na uvedený přehled přesné terminologie operačních systémů.
4.2 Chronologie vzniku Windows 1965 Napsán první víceuživatelský operační systém pod názvem MULTICS. Byl vyvinutý Bellovými laboratořemi (dnešní AT&T) a dále firmami GE a MIT. Neujal se pro svou nadměrnou složitost. 1969 Brian Kernigham vymyslel jméno UNIX 6 pro upravenou verzi MULTICSu, která podporovala práci dvou uživatelů. Jak zlé jazyky tvrdí, právě tento počet stačil ke hře Vesmírné války.
102
1973 Dennis Richtie vyvíjí programovací jazyk pro psaní UNIXu. Jeho varianty popořadě označoval písmeny A, B, C. Poslední již vyhovovala a pomocí ní byl přepsán UNIX 6. 1975 Významný mezník - první osobní počítač. Jmenuje se Altair. Vyrobila ho společnost MITS, obsahoval Intel 8080 a 256 bytů paměti a stál 400 dolarů. Mladík jménem Bill Gates pro něj píše jednoduchý interpret BASICu. (Co myslíte, není to zvláštní, že právě Basic se dnes stal předním programovacím nástrojem Microsoftu nejen pro makra v MS-Wordu, ale i pro Windows? ) 1976 Gary Kildall z Digital Research Inc. vytvořil operační systém CP/M (Control Program for Microcomputers), který se stal hlavním nástrojem pro mnoho osobních počítačů postupně uváděných na trh různými firmami. Ken Thompson (University of California in Berkley) píše novou verzi UNIXu známou jako BSD. 1979 Objevuje se UNIX verze 7, jehož architektura se stala základem pro všechny pozdější verze UNIXu. Jeho program měl kolem 10000 řádek kódu C a 1000 řádek assembleru. 1980 Philip Estrade z firmy IBM dostává za úkol postavit osobní počítač PC. Navrhuje ho ve městě Boca Raton v Kalifornii a staví ho na procesoru Intel 8088 s hodinovou frekvenci 4,77 Mhz, z níž lze odvodit signál pro barevné televizory. První model PC nebude mít disk a bude obsahovat pouhých 64 kB paměti RAM umístěných nešťastně na adrese nula, z velké části díky nevhodné architektuře 8088, zatímco paměť ROM a přímo mapované periférie nadobro zablokují horní část adresového prostoru. Tím pro budoucí verze PC vznikl limit 640 kB RAM, který na dlouho ovlivnil rozvoj výpočetní techniky. Na trh přicházejí nové UNIXy - například lze jmenovat Microsoft a jeho XENIX napsaný na základě licence AT&T, od Hewlett Packard pak HP-UX, od firma DEC zase Ultrix a od IBM produkt AIX. 1981 Firma IBM se snaží získat programy pro ohlášený model IBM-PC. Obrací se na řadu firem mimo jiné na Microsoft a kupuje od něho vylepšenou verzi BASICu. IBM shání operační systém pro PC. To má bohužel příliš málo paměti a žádný disk a nelze pro něj upravit jedinou existující implementaci UNIXu. IBM objednává u Digital Research operační systém CP/M. 1981-duben (čtyři měsíce před uvedením PC na trh) IBM přerušuje jednání z Digital Research (podle neověřených pramenů pro stále rostoucí nároky protistrany) a ptá se Microsoftu, jestli by dokázal rychle napsat operační systém. „Za tak krátkou dobu?“ vzdychl údajně Bill Gates a zamyslel se, „zvládneme to, ale bude to děs a hrůza,“ stvrdil a dal se do díla. Koupil za 20000 dolarů systém 86-DOS na bázi CP/M, který vytvořila firma Seattle Computer Products jako pomocný systém pro testování svých paměťových modulů, a najal jeho autora Tim Patersona, aby ho mírně upravil. Výsledek byl přejmenovaný na MS-DOS a předaný IBM. 1981 srpen - první IBM-PC (s displejem a tiskárnou stojí kolem 5000 dolarů). Jeho operační systém MS-DOS verze 1.0, dnes více známým pod názvem IBM-DOS, zaujímá 12 kB paměti a má 4000 řádek zdrojového kódu. Předností PC se ukázala velká otevřenost systému. IBM ho chápe více jako propagaci svých sálových počítačů než jako nosný produkt, a proto dává k dispozici úplná elektrická schémata a popis funkce BIOSu. Další výrobci mohou bez problémů vyrábět PC v licenci. Právě tento fakt rozšíří počítač po celém světě. AT&T vypustila dva UNIXu pro počítače VAX a PDP se jmény 2.8BSD a 4.1BSD. 1982 Firma Digital Research uvádí na trh operační systém CP/M-86 zcela nekompatibilní s MS-DOSem. Jedná se dobře navržený víceprocesní systém, ale uživatelé o něj nejeví větší zájem. Nechtějí ke drahému počítači kupovat další operační systém, když už jeden mají, a vůbec netouží po výměně všech svých programů za nové. AT&T vypustila novou verzi UNIXu označovanou UNIX Systém III. 103
1983 IBM vytváří PC/XT s pevným diskem. Microsoft pro něj rozšiřuje MS-DOS o některé funkce UNIXu, má nyní už 20000 řádek zdrojového kódu a pracují na něm 4 lidé. 1984 Objevuje se model PC/AT založený na problematickém procesoru 286, který sice přinášel podporu pro víceprocesní režim, ale potíže s adresací řešil pouze částečně. Microsoft pro něj vytváří MS-DOS 3.0. Vzniká dohoda mezi IBM a Microsoftem o vývoji víceprocesního operačního systému OS/2. Podle ní bude IBM pracovat na jádře systému a zatímco Microsoft na grafickém rozhraní. Na trh přichází první počítač Macintosh, který je od začátku koncipovaný jako uzavřený systém patřící plně firmě Apple. Grafické prostředí jeho pozdějších modelů inspiruje Microsoft k návrhu Windows 3.0. 1985 Objevují se Windows 1.0. Jedná se spíš o přepínač aplikací DOSu. Uvedeny X-Windows pro UNIX. Vznikly na půdě Massachusetts Institute of Technology (MIT) a realizují model klient-server a tok událostí použitý později Windows. 1987 Na trh přichází první verze víceprocesního operačního sytému OS/2, společný produkt IBM a Microsoftu. Je nekompletní a umí pouze textovou verzi. Pro řadu uživatelů představoval spíš zklamání. Tento fakt se v budoucnu projevil na důvěře v systém OS/2. 1988 OS/2 je rozšířený o grafické prostředí, ale uživatelé o něj projevili pouze vlažný zájem. Většině z nich k práci stačil MS-DOS. Digital Research uvádí DR-DOS 3.41, operační systém kompatibilní s MS-DOSem, avšak z mnoha vylepšeními (později převzatými MS-DOSem). Zákazníci o DR-DOS nejeví výraznější zájem. Ve vylepšování DR-DOSu bude Digital Research vytrvale pokračovat se stále stejným úspěchem. Svůj obchod století už promeškal. 1990 Microsoft uvedl na trh Windows 3.0, které podporují víceprocesní prostředí velmi nestabilní kooperativní metodou a opírají se od začátku špatný MS-DOS. Vzhled jejich grafické obrazovky (prvek původně vyvíjený pro OS/2) zakryl vady uvnitř. Jejich hlavní výhodou oproti OS/2 byla snazší obsluha. Prodalo se jich 10 miliónů kopií. IBM ruší spolupráci s Microsoftem. 1991 Linus Torvalds z Helsinské univerzity píše LINUX pro GNU projekt, Gnu's Not UNIX v rámci Free Software Foundation založené v roce 1983. První verze se objeví v říjnu 1991. (Gnu žije v Africe a česky se jmenuje pakůň hřivnatý). 1992 Objevují se Windows 3.1 podporující procesor 386. Až do roku 1995 se jich prodá 50 miliónů kopií. OS/2 Warp verze 2.00 - konečně první 32-bitový víceprocesní operační systém, stabilní a s dobrou podporou grafiky. Vyžaduje však minimálně 8 MB v té době velmi drahé paměti (doporučených je 16 MB), VGA kartu a procesor 386. Uživatele příliš nezaujal. Windows 3.1 stačily jen 2 MB RAM, monitor Hercules a pracovaly i na procesoru 286. 1993 Na trh uvedeny Windows NT 3.1, víceprocesorový systém vycházející z nikdy nedokončeného operačního systému OS/3, který firmy Microsoft a IBM kdysi vyvíjely společně. Microsoft dodnes platí licenční poplatky IBM za řadu prvků - namátkou lze jmenovat DDE protokol, COM (Common Object Model) a OLE 2. 1994 Upravená OS/2 Warp verze 3 vyžaduje jen na 4 MB paměti. Lidé netrpělivě vyhlížející 32-bitová Windows o něj konečně mají výraznější zájem, ale stále nedostatečný. Microsoft obecné čekání podporuje sliby o brzkém zahájení prodeje nových Windows. (IBM kdysi stejný způsobem propagovala počítače řady IBM 370. Celých pět slibovala jejich brzké uvedení na trh, jakmile odstraní pár drobných potíží. Možná, že právě tohle bylo dalším důvodem, proč zejména zákazníci z USA dali přednost Windows od Microsoftu.) 1995 Windows 95. Během tří let se jich prodá 150 miliónů kopií. Microsoft se nikterak netají, že na jejich reklamu vynaložil skoro třikrát víc, než na samotný vývoj. Může si to dovolit, jeho pozice se stala téměř neotřesitelnou. 104
1998 Windows 98. 2000 Linux ? / Windows ? / Full House ?
~~ Lze se ptát, proč Windows ovládly svět. Existovaly přece jiné a dokonalejší systémy. Odpověď je prozaická. Sami uživatelé si to přáli. Tajemství úspěchu Windows spočívá v uplatnění známé zásady obchodu, že většina zákazníků kupuje zboží podle tří kritérií - podle obalu, ceny a použití. Kvůli tomu je potřeba navrhnout hezkou barevnou krabici a samozřejmě patřičně velkou, aby konzument dospěl k názoru, že toho za své peníze obdržel hodně, stanovit přijatelnou cenu a vytvořit přístupné ovládání. V případě Windows byla onou hezkou krabicí grafická obrazovka, za níž se většina uživatelů nedívala, výhodnou cenou pak nízké nároky na vybavení počítače a snadným užitím nenáročná instalace a obsluha. Například, zatímco přidání další tiskárny do OS/2 verze 1.2 znamenalo provést 8 složitých kroků, ve Windows 3.0 k tomu stačily pouhé 2 jednoduché úkony. Pokud se nám na Windows něco nelíbí, musíme si uvědomit, že jedině díky zájmu kupujících získal Microsoft téměř monopolní postavení, a samozřejmě se všemi z toho vyplývajícími nepříjemnými důsledky. Dlouhou dobu existovaly dokonalejší operační systémy než Windows, o než se majitelé stolních počítačů téměř nezajímali. Kdyby se tenkrát víc starali o robustnost systému, tak by dnes používali OS/2 a nebo možná X-Windows. Nestalo se. Zdá se tedy, že běžný uživatel nepotřeboval technicky dokonalý OS (tak usilovně propagovaný odborníky z IBM) natolik, aby kvůli němu byl ochotný k vyšším finančním výdajům. Raději se spokojil s menší stabilitou Windows, která mu nevadila, protože používal nenáročné programy, při nichž se neprojevila. Dokonce ani systémy na bázi UNIXu mu neučarovaly - jednak jich bylo příliš mnoho k tomu, aby se v nich vyznal, a jednak měly mnohonásobně složitější instalaci a ovládání než Windows. A k čemu také univerzální přenositelné prostředí, když jste si koupili jeden počítač? Zázrak Windows spočívá v nalezení rozporu mezi míněním odborníků a skutečnými tužbami uživatelů. Jejich masové rozšíření donutilo i protestující odborníky, aby je používali. Zejména průmysl patří mezi velmi konzervativní zákazníky a řada odběratelů vyžaduje aplikace ve Windows z důvodu, že znají jejich ovládání. Důvod je prostý - mají dost starostí se svou vlastní prací a netouží učit se ještě nový OS, když už jeden znají, což je přesně ten fakt, který udržel MS-DOS skoro deset let na výsluní a dnes podporuje Windows. Závěrem se hodí uvést kladný rys monopolu Windows, který spočívá v unifikaci ovládání osobních počítačů. S Windows umí dnes zacházet nejen velký počet techniků, ale i laiků, což výrazně snižuje náklady na zaškolení obsluh. Tento fakt znamená dost vážný argument zejména v situacích, kde je potřeba zacvičit desítky pracovníků neinženýrských profesí.
4.3 Datové typy Programy pro Windows používají řadu vlastních názvů typů, které jsou vytvořené z důvodu kompatibility mezi různými vývojovými prostředími. Definují se v include souboru windows.h pomocí typedef nebo prostřednictvím #define. Některé nejběžnější typy jsou uvedené v následujícím přehledu: VOID FAR
ekvivalent pro C klíčové slovo void & ekvivalent pro C klíčové slovo far
&
V prostředí Win32 budou všechny pointry a funkce typu near bez ohledu na použité specifikace FAR či far. Označování elementů jako far a near se používá výhradně kvůli zpětné kompatibilitě s programy pro Win16. 32 bitové OS znají sice opravdové far pointry (tj. 16-ti bitový selektor + 32 bitový offset), ale ty používají výhradně jejich jádra. Uživatelské programy není dovoleno manipulovat se selektory. V opačném případě dojde ke strukturované výjimce "Nedovolená operace", viz. dále kapitola 8.58.5b.
105
PASCAL CDECL WINAPI CALLBACK BOOL BOOLEAN LPSTR LPVOID BYTE INT UINT WPARAM SHORT WORD USHORT LONG LPARAM LRESULT DWORD ULONG COLORREF
ekvivalent pro C klíčové slovo _pascal ekvivalent pro C klíčové slovo _cdecl (viz. strana 25) označuje funkci FAR PASCAL, viz. dále kapitola 4.4 totéž jako WINAPI Boolean - proměnná nabývající hodnot TRUE (=1) nebo FALSE (=0) totéž jako BOOL long pointer string zero = pointer typu far na řetězec znaků zakončený 0 pointer far bez udání typu proměnné, na kterou ukazuje (void far *) byte (8 bitové číslo bez znaménka) číslu typu int, 32-bitů ve Win32 a 16-bitů ve Win16 číslo typu unsigned int, 32-bitů ve Win32 a 16-bitů ve Win16 totéž jako UINT, 32-bitů ve Win32 a 16-bitů ve Win16 číslo short int, 16-bitů 16-ti bitové číslo bez znaménka totéž jako WORD číslo typu long totéž jako LONG totéž jako LONG 32 bitové číslo bez znaménka totéž jako DWORD Rudá, zelená, modrá (RGB) - hodnota barvy (32 bitové číslo)
Dále se využívají některá makra s parametrem, například: #define LOBYTE(w) ((BYTE)(w)) // výběr #define HIBYTE(w) ((BYTE)((UINT)(w) >> 8)) // výběr #define LOWORD(l) ((WORD)(l)) // výběr #define HIWORD(l) ((WORD)((DWORD)(l) >> 16)) // výběr
dolního bytu čísla WORD horního bytu čísla WORD dolního WORD z číslo long horního WORD z čísla long
#define MAKELONG(low, high) ( (LONG)(((WORD)(low)) | (((DWORD)((WORD)(high))) << 16)) ) // vytvoří z parametru low a high (dolní a horní slovo) číslo long #define RGB(r,g,b) ( (COLORREF)(((BYTE)(r) | ((WORD)(g)<<8)) | (((DWORD)(BYTE)(b))<<16)) ) // vytvoří barvu z parametrů r,g,b (hodnoty barev rudá, zelená, modrá) Při volbě jmen proměnných se často dodržuje zásada, že jméno začíná písmenem specifikujícím typ proměnné, například: DWORD dwData1, dwData2;
// definice dvou proměnných DWORD
Oblíbené prefixy typu používané u identifikátorů jsou: sz - string zero = C řetězec zakončený 0 str - totéž jako sz w - WORD l - LONG dw - DWORD p - pointer lp - long pointer = pointer typu far lpstr - LPSTR = pointer typu far na řetězec znaků zakončený 0 np - near pointer
4.4 Callback funkce V kapitole nazvané Chyba! Neznámý argument přepínače. na straně 25 se uváděly čtyři různé způsoby, kterými se dají předávat argumenty funkci. Na základě specifikovaného typu vybírá 106
kompilátor vhodný překlad jednak pro funkci a jednak pro všechna její volání. Uživatel si pro svoje interní funkce může vybrat libovolný způsob volání. Pokud se ale vytváří funkce, která slouží jako externí vstupní bod a volá z OS, pak ta musí používat předepsaný způsob volání. Ten se ve Windows aplikacích specifikuje CALLBACK nebo WINAPI. Například: void CALLBACK TimerProc(HWND hwnd, UINT msg, UINT idTimer; DWORD dwTime ); BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); Funkci TimerProc volá OS, pokud byla přidělená nějakému časovači a už uplynul nastavený interval. DllEntryPoint zase slouží pro inicializaci dynamické knihovny (o tom v kapitole 9.2). Obě deklarace, s CALLBACK a s WINAPI, specifikují překlad ve tvaru vhodném pro volání z OS. To vy aduje pøedávání parametrù urèené normou _pascal a ve Win16 navíc i pomocí funkce ' typu _far. Win16 používají deklarace CALLBACK, zatímco ve Win32 se lze uplatnit obě deklarace (ty mají v nich zcela totožný efekt), avšak dává se přednost WINAPI. Pro další výklad si zavedeme pojem callback funkce(= zpětné volaná funkce), jímž budeme označovat funkci volanou operačním systémem při nějaké vhodné příležitosti. Například "callback funkce hlavního okna aplikace", bude odkazovat na funkci deklarovanou s použitím CALLBACK nebo WINAPI, kterou OS volá, když potřebuje předat hlavnímu oknu nějakou informaci.
4.5 Server a klienti Architektura OS Windows se opírá o strukturu zvanou server-klient. V roli serveru vystupuje OS, zatímco klienty jsou běžící procesy a jejich vlákna. Komunikace probíhá oběma směry. Server, tj. OS Windows, vykonává operace požadované klientem, dále zprostředkovává distribuci zpráv mezi klienty a některými jimi vytvořenými prvky a zasílá klientům zprávy hlásící výskyt události. Klienty OS jsou procesy. Ty si od OS odebírají zprávy pomocí volání jeho služeb a reagují na ně podle potřeby. Procesy ve Win32 nemají přímý přístup ke vstupním a výstupním perifériím počítače, a proto musí veškeré operace s nimi provádět výhradně prostřednictvím služeb OS nebo posíláním zpráv. !
4.5a
Handle
Každý dynamický objekt, který Windows vytvoří, dostane přidělený handle (manipulátor, ovladač), který lze považovat za jakousi analogii evidenčního čísla. Handle se musí uvádět jako parametr při všech voláních systémových funkcí, které s objektem manipulují, a platí až do zániku objektu, tj. až do zavolání služby OS, kterou se objekt zruší. Handle bývá zpravidla číslem typu UINT a svým významem většinou, ale nikoliv vždy, odpovídá adrese nějaké interní struktury OS. S tou však nelze manipulovat přímo, ale pouze pomocí služeb systému, a proto představa handle jako obyčejného evidenčního čísla dobře vystihuje situaci. Číselná hodnota handle je jedinečná výhradně uvnitř skupiny podobných prvků, například v množině všech existujících ikon. Z důvodu vyloučení záměny se proto zavedly odlišné typy pro jednotlivé handle skupiny. Například: HBITMAP - handle bitmapy HCURSOR - handle kursoru myši HFONT - handle fontu HGLOBAL - handle alokovaného bloku paměti '
Funkce _far se ve Win16 volají tak, že se na zásobník ukládá far pointer na návratovou adresu, tj.16-ti bitový selektor a offset, zatímco _near volané funkce tam dávají pouze offset, viz. kapitola 1.31.3b. Naproti tomu ve Win32, jak bylo řečeno již dříve, se klíčové slovo _far ignoruje. Uživatelské aplikace volají funkce vždy jako _near, přičemž se na zásobník dává 32-bitový near pointer na návratovou adresu. ! Ve Win16 existoval omezený přístup na některé vstupní a výstupní porty.
107
HICON - handle ikony HPEN - handle pera ke kreslení na obrazovku HWND - handle okna Následující příklad ukazuje použití handle pro změnu kursoru myši: HCURSOR hcurPredchozi; // proměnná pro handle kursoru myši HCURSOR hcurHodiny; // proměnná pro nová handle kursoru myši hcurHodiny = LoadCursor(NULL, IDC_WAIT); /* zjisti handle systémového kursoru ve tvaru přesýpacích hodin */ hcurPredchozi = SetCursor(hcurHodiny); /* nastav kursor myši na tvar hodin a vrať handle dříve používaného kursoru */ /* provedeme nějakou časově náročnou operaci */ SetCursor(hcurPredchozi); // vrácení původního kursoru Přísnou syntaktickou kontrolu typů handle lze vypnout, zrušíme-li předdefinovaný symbol preprocesoru STRICT před direktivou pro vložení windows.h a pak můžeme pro všechny handle použít univerzální typ HANDLE. Tím samozřejmě zvýšíme pravděpodobnost chyby #undef STRICT // zrušení přísné syntaktické kontroly #include <windows.h> // vložení základních definic Windows /* Nyní lze pro všechny objekty s handle používat jediný typ HANDLE */
4.5b
Okno
Okno představuje základní grafický prvek Windows. Oknem jsou všechny aktivní grafické elementy - jako třeba tlačítko, scroll bar, editační pole, list box, dialog, dokonce i některé statické texty. Každé okno dostane při svém vzniku přidělený handle, které má přidělený typ HWND, a většinou si zaregistruje funkci typu callback (viz. kapitola 4.4). Tu OS zavolá, kdykoliv má pro okno zprávu. Systém oken tvoří hierarchickou strukturu. Každé okno může, ale nemusí mít svého vlastníka (parent) a podřízené prvky (childern). Význam vztahu podřízenosti se projeví především při zániku oken. S oknem se automaticky ruší všechna jemu podřízená okna. Hierarchickou strukturu lze prohledávat podobně jako les, tj. množinu několika stromů, použijeme-li systémovou funkci GetWindow. HWND GetWindow(hwnd, fuRel); kde hwnd je handle okna a fuRel vztah, GW_OWNER GW_CHILD GW_HWNDFIRST, GW_HWNDLAST GW_HWNDNEXT, GW_HWNDPREV
který k němu má hledané okno: - vlastník; - první podřízené okno; - první, poslední okno na úrovni shodné s hwnd; - další, předchozí okno na úrovni shodné s hwnd.
Každé okno lze zobrazit v rozličném stylu podle toho, jakou kombinaci základních prvků okna použijeme. Windows nabízejí možnost oknu přidělit: • titulek (title), s názvem okna; • ohraničení (border), které tvoří tenká čára kolem okna; • rám (thick frame), za nějž lze vzít myší a měnit tak velikost okna;
108
Aplikace
HWND hW11 Podřízené okno 1.1 hParent = hW1
HWND hW1
HWND hW12
Hlavní okno 1
Podřízené okno 1.2 hParent = hW1
hParent = 0
HWindow = hW13 Podřízené okno 1.3 hParent = hW1
HWND hW21 Podřízené okno 2.1
HWND hW2
hParent = hW2
Hlavní okno 2
HWND hW22
hParent = 0
Podřízené okno 2.2 hParent = hW2
HWND hW221 Podřízené okno 2.2.1
HWND hW222
hParent = hW22
Podřízené okno 2.2.2 hParent = hW22
Obr. 4-1 Hierarchická struktura oken • systémové menu (system menu) - ikona v levém horním rohu okna, která zobrazí nabídku zavřít, minimalizovat a podobně; • tlačítka pro zmenšení okna do ikony, pro roztažení okna na celou obrazovku a zmenšení okna na normální velikost (minimize, maximize and minmaximize box); • vodorovnou a svislou lištu pro posun obrazu (horizontal and vertical scroll bar). Různou kombinací prvků, můžeme vytvořit celou řadu rozličných oken, např. tlačítko je okno, které má titulek a ohraničení. Lze nastavit umístění okna: • velikost - šířku a výšku (width, height); • pozici na obrazovce - x a y souřadnici levého horního rohu. 109
a dále okno může (ale nemusí) vlastnit některé prvky typu resource jako - menu, ikonu, akcelerační tabulku kláves pro zrychlený výběr položek z menu. Chceme-li připojit menu, zadáváme pouze jeho tvar. Windows ho samy vykreslují a posílají nám zprávy popisující, co uživatel s menu provádí. Nejvýše jedno okno může být v daném momentu aktivní, což se označuje termínem, že okno získalo focus. Oknu majícímu focus se zasílají zprávy od klávesnice a proces, který je jeho vlastníkem, se zpravidla dostává do popředí, tj. OS mu přiděluje více časům než ostatním procesům, označovaným jako procesy běžící na pozadí. Focus lze přepínat nejen myší a tabulátorem, ale i z programu (systémová funkce SetFocus). Okno může mít také různou viditelnost: • skryté okno (hidden) se neukazuje na monitoru; • maximalizované okno zaujímá celou plochu obrazovky; • minimalizované okno se zobrazuje pouze ve formě ikony; • normální okno zaujímá část obrazovky. Kromě toho lze nastavit, aby okno získalo speciální vlastnosti - například bylo trvale v popředí (topmost) i za situace, že nemá focus. Povoluje se vytvořit ho i jako transparentní a ! nechat prosvítat prvky pod ním.
4.5c
Zprávy ve Win32
Zprávy generují Windows a aplikace. Windows vytvoří zprávu pro každou vstupní událost, například při stisku klávesy či pohybu myši. Další zprávy vznikají při změně prostředí, jako při úpravě velikosti okna anebo při změně systémového fontu. Aplikace mohou posílat zprávy svým vlastním oknům anebo oknům ostatních aplikací. Zpráva představuje datový paket, obsahující následující údaje (ve Win32 reprezentované 32bitovými čísly, zatímco ve Win16 má pouze lParam 32 bitů a ostatní data 16 bitů): HWND hwnd; // handle prvku typu okno UINT uMsg; // identifikátor typu zprávy WPARAM wParam; // první parametr zprávy LPARAM lParam; // druhý parametr zprávy Člen hwnd představuje handle okna, které je spojeno (asociováno) se zprávou a většinou má význam adresáta zprávy. Pokud takové okno neexistuje (to může nastat například u zprávy WM_TIMER - doběhnutí časovače), pak se parametr hwnd se rovná 0. Číslo uMsg identifikuje zprávu a na jeho základě zvolí příjemce reakci na zprávu. Například zpráva WM_PAINT má číslo uMsg rovné 15 a specifikuje požadavek, že došlo ke změně části kreslicí plochy okna a je potřeba ji znovu nakreslit. Číslo uMsg současně určuje i význam parametrů wParam a lParam, které specifikují pomocná data. Pokud příslušný parametr není použitý, jeho hodnota by se měla rovnat 0. Například, zpráva WM_PAINT má oba parametry nulové, zatímco zpráva WM_SETTEXT (nastavení textu, třeba titulku okna nebo tlačítka) má následující hodnoty parametrů: hwnd = ? - handle okna, jehož text měníme, například titulek tlačítka, uMsg = WM_SETTEXT - konstanta rovná 12, wParam = 0 - není použitý a musí být rovný 0, lParam = pointer typu LPSTR - ukazuje na text. Zprávy uvnitř programu se často ukládají do struktury MSG, která kromě zmíněných veličin obsahuje i čas příchodu zprávy a polohu kurzoru myši.
!
Transparentnost lze zadat pouze ve fázi vytváření okna viz. dále kapitola 5.35.3a. Během té se může rovněž specifikovat i vlastnost topmost. Ta se kromě toho dá oknu přiřadit, respektivě zrušit, i službou SetWindowPos.
110
Windows používají dvě metody pro distribuci zpráv: 1. zaslání zprávy přímo té funkci, která ji obsluhuje, 2. uložení zpráv do fronty, ze které si je vlákno samo odebírá. První způsob je nejčastější. Aplikace si zaregistruje obslužnou callback funkci. Tu Windows zavolají, kdykoliv se vyskytne zpráva pro ni určená. Myš
Klávesnice Časovače
Systémová fronta
Vlákno 2 Smyčka frontových zpráv Fronta vlákna 2
Vlákno 1 Smyčka frontových zpráv GetMessage … DispatchMessage
Fronta vlákna 1
CALLBACK funkce zpráv okna A
Nefrontové zprávy
CALLBACK funkce zpráv okna B SendMessage A PostMessage A Obr. 4-2 Zpracování zpráv ve Win32
Některé zprávy se neposílají callback funkci, ale zařazují se do fronty, v níž čekají na zpracování. Frontu tvoří systémová paměť typu FIFO (first-in, first-out - první dovnitř, první na řadě) a zprávy do ní ukládané se nazývají frontovými zprávami (queued messages). Jde především o vstupní zprávy, například od myši a od klávesnice, jako třeba pohnutí myší nebo stisk klávesy (zprávy WM_MOUSEMOVE a WM_KEYDOWN). Dále se do fronty řadí ještě zprávy WM_PAINT, ! WM_QUIT (ukončení aplikace) a WM_TIMER, hlášení o uplynutí intervalu časovače. Windows zařazují nové zprávy vždy na konec fronty s výjimkou zprávy WM_PAINT, žádosti o překreslení okna. Vzhledem k tomu, že grafické operace bývají zdlouhavé, Windows při příchodu WM_PAINT zprávy prohlédnou celou frontu, jestli v ní zpráva WM_PAINT už nečeká na zpracování. Najdou-li ji, sloučí obě zprávy WM_PAINT v jedinou, čímž se vylučuje opakované překreslování okna. Win32 obsahují jednu systémovou frontu a dále samostatné fronty pro každé vytvořené vlákno. Aplikace má vždy nejméně jedno vlákno, a proto vlastní nejméně jednu frontu zpráv. !
Pro časovače existují dva možné způsoby jejich obsluhy, zasíláním zpráv WM_TIMER anebo zaregistrováním obslužné callback funkce, kterou OS volá po uplynutí zadaného intervalu.
111
Kdykoliv uživatel pohne myší, stiskne nebo uvolní klávesu, potom driver, který příslušné zařízení obsluhuje, převede událost na zprávu a tu umístí v systémové frontě. Z této fronty Windows odebírají popořadě zprávy, analyzují je, aby rozhodly o tom, komu je mají doručit. Každou zprávu odešlou do fronty vlákna, které vlastní jejího adresáta, jímž bývá většinou okno. Vlákno tak dostává do své vlastní fronty veškeré zprávy od myši, klávesnice a časovačů, které si samo vytvořilo. Ukázka zpracování zpráv je znázorněna na Obr. 4-2. Pod vlákny si můžeme představit jednu aplikaci, která má dvě vlákna, nebo dva samostatné aplikace, každá s jedním vláknem.Vlákno odebírá zprávy čekající ve frontě voláním systémových funkcí, většinou pomocí GetMessage. Smyčka zpracovávající frontové zprávy může vypadat například takto: MSG msg; // struktura pro uložení zprávy while (GetMessage(&msg, NULL, 0, 0)) // čtení zprávy, která je ve frontě na řadě {
if ( ! NasZnak(&msg) ) // naše vlastní funkce, která zpracovává speciální znaky { TranslateMessage(&msg); // předzpracujeme zprávy z klávesnice DispatchMessage(&msg); // odešleme výsledek adresátovi } }
Zpracování probíhá v cyklu while. Zpráva načtená GetMessage se napřed analyzuje ve funkci NasZnak, která detekuje a zpracovává zprávy vyžadující obsluhu na úrovni vlákna. Ostatní zprávy se předzpracují pomocí funkce TranslateMessage, která dvojice zpráv hlásících stisk a uvolnění klávesy (WM_KEYDOWN a WM_KEYUP) slučuje v jednou zprávu WM_CHAR, napsaný znak. Výsledek TranslateMessage se funkcí DispatchMessage odešle adresátovi určenému parametrem hwnd struktury MSG. Smyčka while se opakuje, dokud funkce GetMessage nevrátí hodnotu FALSE, značící přijetí zprávy WM_QUIT, což znamená konec vlákna. Způsob odebírání zpráv může mít bezpočet variant. Zprávy není nutné posílat pomocí DispatchMessage, ale lze použít vlastní mechanismus. Dále lze vyjmout jen specifickou zprávu, funkcí GetMessage, a ostatní zprávy ponechat ve frontě, čímž lze obejít pořadí zpracování zpráv. Obsah fronty se může analyzovat PeekMessage, která zprávy nevyjímá, ale pouze čte. Vláknu stačí jedna smyčka frontových zpráv, bez ohledu na počet vytvořených oken. Každá zpráva obsahuje handle okna, jemuž je určena, a podle toho ji funkce DispatchMessage doručí vhodnému adresátovi. Ve Win32 dokonce aplikace nemusejí mít smyčku zpráv, pokud nepotřebují frontové zprávy, například provádí-li čistě paměťové operace. Program může posílat zprávy několika funkcemi, například pomocí SendMessage - pošli zprávu a čekej na její vyplnění PostMessage - ulož zprávu do systémové fronty a pokračuj v programu Funkce SendMessage čeká, dokud není zpráva není obsloužena a neobdrží od adresáta její výsledek. Jejím opakem je funkce PostMessage, která zprávu pouze uloží na konec fronty a vrací hodnotu TRUE, bylo-li uložení úspěšné, v opačné případě dává FALSE. Poznámky: 1. Častou programátorskou chybou bývá ignorování výsledku funkce PostMessage a automatický předpoklad, že ve frontě zpráv je stále dost místa na uložení nové zprávy. 2. SendMessage čeká na výsledek zaslané zprávy. Pokud posíláme zprávu jinému procesu nebo vláknu, předáváme mu tím samozřejmě i aktivitu. Pokud o ni adresát přijde během zpracování zprávy a nevratně ji předá dalšímu procesu či vláknu, třeba zpět našemu programu, dojde k zablokování. Náš program bude čekat na dokončení operace vyžádané SendMessage, ale k tomu nikdy nedojde, protože adresát není již aktivní. Výsledkem bude nekonečné čekání, tzv. deadlock. Zájemci naleznou bližší popis problému u funkcí InSendMessage, ReplyMessage a SendMessage, a při vyhledání pojmu deadlock v souboru nápovědy pro Windows, který je součástí každého překladače. 112
3. Zpracování zpráv ve Win16 se liší od popsaného mechanismu především těmito body: • existuje v nich dlouhá systémová fronta společná pro všechny aplikace, ale velmi krátké fronty jednotlivých aplikací (pouze pro 8 zpráv); • nepodporují více vláken a každá aplikace má právě jedno vlákno; • využívají volání GetMessage k přepínání mezi jednotlivými aplikacemi, tzv. kooperativní multitask, a kvůli tomu musí v každé aplikaci bezpodmínečně existovat smyčka frontových zpráv, jinak dojde k zablokování celého OS. Není-li ve Win16 volána GetMessage, nebo nějaký její ekvivalent, OS nedokáže přepínat procesy; • Win16 zprávy tvoří podmnožinu Win32 zpráv, avšak mnohé zprávy se v nich liší formátem parametrů wParam a lParam. Případné rozdíly mezi nimi nevadí při uplatnění objektových knihoven, protože jsou ošetřené na úrovni předdefinovaných metod.
4.6 Architektura programu pro Win32 Win32 umožňují vytvářet dva typy základní aplikací: • Console aplikace nepoužívající okna a pracující výhradně s textovou obrazovkou; • GUI aplikace (Graphic User Interface), které využívají grafické prostředí.
4.6a
Console aplikace
Pro Console aplikace spravuje smyčku zpráv OS a současně pro ně emuluje i textovou obra!! zovku pomocí služby zvané pipe. Díky tomu Console aplikace mohou číst znaky z klávesnice a psát na displej pomocí běžných funkcí jako getch, printf. Struktura zdrojového kódu jejich programů se tak podobá programům pro MS-DOS. Stejně jako ony začínají funkcí main a lze v nich používat většinu knihovních funkcí jazyka C známých z MS-DOS programů. Nepovolují se v nich pouze operace svázané s prostředím MS-DOSu, jako příkazy pro práci s přerušovacími vektory, s BIOSem a podobně. Údaje o možnosti použití jednotlivých funkcí v Console aplikacích bývají uvedené v nápovědách, obvykle jako Portability anebo Quick Info. Console aplikace nesmějí využívat grafické možností Windows, protože jim není dovoleno vytvářet okna, avšak nabízejí veškeré ostatní výhody 32-bitového prostředí - 4 GB virtuální paměti, podporu více vláken, mapování souborů, sdílení paměti, dynamické knihovny a jiné. Tyto možnosti se budou probírat v kapitole 7.5 věnované přínosům Win32 a v kapitole 9 zaměřené na dynamické knihovny. Console aplikace se od GUI aplikací se odlišují také tím, že operace každého jejich vlákna tvoří jedinou dlouhou posloupnost, a proto je lze popsat souvislým vývojovým diagramem.
4.6b
GUI aplikace
Pouze GUI aplikace smějí využívat grafické rozhraní Windows. Jejich vstupním bodem, analogií funkce main, je WinMain, kterou Windows zavolají po vytvoření procesu ze souboru aplikace a po jejím ukončení, zpravidla operací return, je proces zrušen. Důležitým pojmem pro WinMain je handle na instanci aplikace. Ten předají aplikaci Windows při jejím spuštění a musí se uvádět jako argument řady systémových služeb, třeba při čtení resource. Nedá se však pomocí něho manipulovat s procesem jako s celkem. Termín instance pochází z prostředí Win16, v nichž stejné paralelně běžící aplikace sdílely kód programu. Například, byl-li dvakrát spuštěný kalkulátor, pak oba procesy kalkulátoru dostaly přidělené svoje vlastní paměťové oblasti pro uložení dat, ale používaly totožný kód programu. Dále sdílely i některé prvky, například registrace oken, aby se zmenšil objem interních dat, neboť v době Win16 měly počítače relativně malé paměti (ve srovnání s dnešním stavem). Procesy se společný kódem se nazývaly instance aplikace. !!
Služby pipe (poštovní potrubí) nejsou ve skriptech probírané, s výjimkou stručné zmínky v kapitole 10.1.
113
Ve Win32 se však procesy považují za zcela samostatné entity a nemají apriori žádné společné prvky. Nicméně pojem zůstal, i když každá instance aplikace je zcela nezávislý proces. !" Funkce WinMain, vstupní bod uživatelské aplikace, má deklaraci: int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow ); kde: hInstance představuje handle instance aplikace; hPrevInstance - handle předchozí instance aplikace, ve Win32 je vždy rovný 0; lpszCmdLine - pointer na C řetězec, tj. zakončený 0, v němž je uložený konec příkazového řádku, jímž byla aplikace spuštěná (chybí v něm začátek specifikující jméno souboru aplikace); nCmdShow specifikuje, jak se má zobrazit okno aplikace. Většinou nabývá hodnot: SW_HIDE okno bude skryté; SW_SHOWMAXIMIZED okno se zobrazí na celou obrazovku; SW_SHOWMINIMIZED ukáže se jenom ikona okna; SW_SHOWNORMAL okno zaujme nějakou část plochy obrazovky. Úkolem WinMain je inicializovat aplikaci a potom provádět smyčku frontových zpráv. Ta končí, dostane-li aplikace zprávu WM_QUIT. Zprávu WM_QUIT lze poslat například systémovou funkci PostQuitMessage(nExitCode), v níž argument nExitCode udává důvod ukončení aplikace a rovná se 0, jede-li o normální zakončení, tj. bez chyby. Parametr nExitCode bude obsažený jako wParam zprávy WM_QUIT a funkce WinMain by ho měla vrátit jako svoji návratovou hodnotu příkazem return nExitCode. WinMain provádí pouze smyčku zpráv a všechny další aktivity GUI aplikace vykonávají callback funkce, které OS volá při výskytu jednotlivých zpráv, tj. pokud dojde k nějaké události. Vzhledem k nutnosti obsloužit přicházející zprávy a dále k faktu, že počítač má obvykle pouze jediný procesor, lze stanovit následující zásady pro architekturu kódu GUI aplikace: • existuje v ní nejméně jedna smyčka frontových zpráv, která pracuje po celou dobu běhu aplikace, a nejméně jedna funkce typu callback, kterou Windows volají při výskytu nefrontové zprávy; • každá callback funkce, obsluhující nefrontové zprávy, vykoná pouze odezvu na zprávu a poté ihned vrátí řízení zpět Windows (příkazem return), aby ty mohly zpracovat další zprávy a zavolat jiné callback funkce obsluhující další události; • nutnost vracet vždy řízení OS znamená, že program GUI aplikace nelze popsat spojitým vývojovým diagramem, ale tvoří ho několik samostatných bloků, z nichž každý je obhospodařovaný nějakou callback funkcí volanou z OS. V prvním přiblížení můžeme proto GUI aplikaci považovat za řadu samostatných funkcí, které jsou spouštěné vnějšími událostmi a vzájemně sdílejí datové zdroje (resources), globální data a pomocné služby. Tento způsob architektury se nazývá řízený událostmi - event driven; !# • Windows volají obslužné callback funkce, ale tyto funkce se v řadě případů nesmějí volat z vlastního programu aplikace. Podobný pokus může skončit havárií (např. v případě WM_PAINT). Callback funkce obsluhující zprávy lze aktivovat výhradně posláním vhodné zprávy. Stručně řečeno: "GUI aplikace jsou zprávy, zprávy a zprávy." !"
Ve Win32 jsou kódy jednotlivých procesů od sebe oddělené mapováním adresového prostoru. Je-li pětkrát spuštěný kalkulátor, není do paměti pětkrát nahraný jeho kód, avšak díky mapování to tak vypadá. Blíže se o tom bude hovořit v kapitole 9 věnované dynamických knihovnám a kapitole 8.7 o tvorbě sdílené paměti. !# Výrobní proces, obvykle řízený automaty PLC, představuje také příklad systému řízeného událostmi. PLC programy, podobně jako GUI aplikace, se rozpadají na oddělené bloky. Ty se však volají periodicky a samy si testují výskyt příslušné události, jako třeba změnu stavu čidla. Ten způsob je sice o hodně pomalejší než posílání zpráv, ale zato mnohem robustnější, protože snižuje riziko ztráty zprávy a zablokování programu (deadlock).
114
WinMain Inicializace aplikace Vytvoření okna Připojení menu k oknu
Spuštění aplikace
Smyčka frontových zpráv Fronta zpráv
Menu okna spravované Windows Exit -> zpráva konec Zobraz -> zpráva kresli
…
DispatchMessage return nExitCode;
WM_PAINT
WM_QUIT
GetMessage
CALLBACK funkce okna zpráva = = konec
KonecAplikace PostQuitMessage(nExitCode=0); return
zpráva = = kresli
FunkceKresli Vypočti parametry obrazu Pošli zprávu WM_PAINT
zpráva = = WM_PAINT
return
return
Globální data aplikace
FunkcePAINT Nakresli obraz return
Obr. 4-3 Pøíklad jednoduché GUI aplikace Další omezení klade na GUI aplikace sdílení displeje. Ten se, na rozdíl od MS-DOSu, nechová jako paměť, v níž by nakreslené objekty zůstávaly, dokud je sama aplikace nesmaže. Každý grafický prvek může být kdykoliv přepsaný jinými běžícími aplikacemi nebo OS. Windows chrání obsah paměti displeje pouze částečně. Automaticky vykreslují třeba rámečky kolem oken, titulky, menu a tlačítka. Kreslicí plochu okna schovají do paměti a opět obnovují výjimečně, například při jejím překrytí systémovým dialogem nebo menu. Ve většině případů, jako třeba při zakrytí části okna jiným oknem, bude kus původního grafického okna smazaný a použitý pro zobrazení nových dat. Ve chvíli, kdy se skrytá plocha okna opět dostane do popře115
dí, Windows pouze pošlou příslušné callback funkci, která je oknu přiřazená, zprávu WM_PAINT (žádost o obnovení obsahu kreslicí plochy). GUI aplikace si proto musí v paměti nepřetržitě uchovávat potřebné informace o každém prvku, který zobrazila na kreslicí ploše svého okna, aby ho dokázala kdykoliv obnovit. V nejjednodušším případě, za předpokladu, že aplikace ukazuje pouze statický obrázek, přibližuje strukturu GUI aplikace Obr. 4-3. Aplikace po svém spuštění (tzn. v okamžiku, když OS vytvořil úlohu ze souboru aplikace a zavolal příslušnou callback funkci WinMain) provedla svoji inicializaci a vytvořila svoje hlavní okno postupem, který bude tématem další kapitoly. Hlavnímu oknu přiřadila menu a poté začala provádět smyčku frontových zpráv. Když obdržela frontovou zprávu WM_PAINT, odeslala jí pomocí služby DispatchMessage zaregistrované callback funkci hlavního okna. Obdobně naložila i s dalšími zprávami. Smyčka zpráv se přerušila, když GetMessage narazila na zprávu WM_QUIT, což ohlásila vrácením FALSE. Program poté vykonal příkaz "return nExitCode;", v němž použil hodnotu nExitCode obdrženou v parametru zprávy WM_QUIT. Tím se ukončila WinMain, v důsledku čehož OS zrušil i úlohu vytvořenou ze souboru aplikace. Veškerou ostatní činnost aplikace dirigoval OS, který aktivoval obslužné funkce po obdržení příslušných zprávy, v ukázce na Obr. 4-3 posílaných především z menu. Menu je plně obsluhováno a zobrazováno OS. V případě, že si uživatel vybere z jeho nabídky nějakou položku prvek, OS zavolá callback funkci okna s parametry tvořenými číslem zprávy WM_COMMAND (= 273) a identifikátorem zvolené položky menu. Na obrázku jsou zprávy od položek menu označené jako konec a kresli. Každá zpráva bude mít za následek zavolaní callback funkce okna. Ta obsahuje příkaz switch větvící operace podle přijatých zpráv. Při zprávě kresli se z něho zavolá FunkceKresli. Ta však nekreslí do okna, ale pouze vypočte nový stav obrazu, který uloží do globálních proměnných. Poté odešle oknu zpráva WM_PAINT na znamení, že jeho momentální obsah nesouhlasí s požadovaným stavem. Zpráva WM_PAINT patří mezi frontové zprávy. OS ji proto zařadí do fronty, kde čeká, dokud si ji aplikace neodebere ve smyčce frontových zpráv a neodešle ji pomocí DispatchMessage callback funkci okna. Té OS pak předá v parametrech zprávu WM_PAINT. V ukázaném příkladu se při WM_PAINT z callback funkce volá FunkcePAINT, která z dat uložených v globálních proměnných aplikace vykreslí okno. Jelikož stav obrazu je uložený v globálních proměnných, OS může kdykoliv, pokud to potřebuje, klidně smazat okno a použít jeho plochu k jiným účelům. K tomu dojde například při překrytí okna jinou aplikací a podobně. Bude-li potřeba původní okno obnovit, stačí poslat jeho callback funkci zprávu WM_PAINT a okno se samo nakreslí.
4.7 Zdrojový text aplikace pro Windows Aplikace pro Windows nevystačí s klasickým kódem programu v jazyce C, ale potřebují další soubory pro popis dat v resource, pro určení služeb ve volaných dynamických knihovnách a pro popis způsobu vytvoření procesu. Jednotlivé části zdrojových souborů aplikace mají obvykle tyto přípony: Program v jazyce C *.C - zdrojový kód v jazyce C *.CPP - zdrojový kód v jazyce C++ *.H - include - soubor obsahující deklarace používané v C programech
116
Resource *.RC
*.RH
- resource soubory, obsahující zdrojové kódy dat připojených k aplikaci, jimiž se definují bitmapy, ikony, tvary menu, speciální fonty a další prvky. Tyto soubory lze je vytvářet v textové podobě, buď přímo anebo pomocí speciálních editorů jako Resource Workshop. Lze je také zcela vynechat, protože veškeré prvky, které se do nich dávají, si proces může vytvářet i jinými způsoby a jejich zahrnutí do resource pouze zjednodušuje zdrojový kód programu. - include - soubor obsahující deklarace konstant (pomocí příkazů #define), které jsou použité v souboru *.RC a na něž se odkazuje v C programech. Soubor *.RH se vkládá příkazem include do C části programu i do resource.
Knihovny *.DLL - dynamická knihovna obsahující vnější funkce připojované operačním systémem k aplikaci až v době, kdy je spuštěna, tj. v okamžiku, když je z ní OS vytváří proces. Blíže viz. kapitola 9. *.LIB - odkazy na vstupní body použitých dynamických knihoven. Tento soubor vytvoří z existující dynamické knihovny program zvaný "Import Librarian", který je běžnou součástí překladačů. K tomu, aby se daly volat funkce definované v dynamické knihovně, potřebujeme navíc odpovídající soubor include (*.h), který obsahuje deklarace funkcí a proměnných obsažených v dynamické knihovně. Bez něho můžeme sice příslušnou službu zavolat, OS nám to umožní, ale nevíme, v jakém formátu jí máme předat parametry. Odpovídající soubor *.h nám musí dodat autor knihovny. Z jejího kódu ho nelze jednoduše zjistit. *.LIB - statické knihovny - mají stejnou příponu jako odkazy na dynamické knihovny, ale na rozdíl od nich obsahují úplný kód funkce. Modul *.DEF - popis způsobu tvorby procesu z aplikace a také určení části STUB souboru aplikace. Pro většinu Win32 aplikací nemusíme *.DEF soubor psát a můžeme nechat překladač, aby použil jeho standardní tvar. Soubory *.DEF mají význam jen při vytváření dynamických knihoven nebo Win16 aplikací. Postup sestavení aplikace je ukázán na Obr. 4-4. • Textové soubory, které lze opravovat běžným editorem, mají dvojité rámečky. • Programy, které soubory zpracovávají, jsou označené zakulacenými tečkovanými rámečky. • Tečkovaná čára specifikuje operaci vložení souboru (#include). • Plná šipka udává směr zpracování. • Čerchovaná spojka zvýrazňuje, že soubory náleží k sobě. Samotný program na Obr. 4-4 je rozdělen dvou bloků, na prog a prog1, které jsou napsané v jazyce C++ ve dvou souborech prog.CPP a prog1.CPP. K nim odpovídající deklarace se nacházejí v souborech prog.H a prog1.H. V programu se používají pomocná data popsaná souborem prog.RC. Konstanty nutné ke specifikaci těchto dat, například pro jejich načtení, jsou umístěné v souboru prog.RH, který se vkládá jak do prog.RC tak do C++ kódů prog.CPP a prog1.CPP. Dále program využívá služby z externí dynamické knihovny dynamic.DLL. Programem Import Librarian byl proto vytvořený soubor dynamic.LIB obsahující nutné informace o vstupních bodech služeb. Ten byl přidaný do seznamu souborů, které linker zpracovává (tj. do projektu integrovaného prostředí překladače). Do C++ programu, jenž využívá služby knihovny, se musí navíc vložit soubor s deklaracemi dynamic.H dodaný autorem dynamické knihovny, aby překladač věděl, jakým způsobem má žádané funkce volat.
117
dynamic.H
prog.H
prog1.H
prog.RH
prog.CPP
prog1.CPP
prog.RC
static.H
C++ překladač
C++ překladač
Resource překladač
static.LIB
prog.OBJ
prog1.OBJ
prog.RES
Linker
dynamic.LIB
~prog.EXE
prog.DEF
Import Librarian
Binder
STUB.EXE
dynamic.DLL
prog.EXE
Widnows – vytvoření procesu Obr. 4-4 Postup sestavení aplikace Kromě dynamické knihovny se volají i funkce ze statické knihovny static.LIB. Tu stačí pouze připojit do seznamu pro linker, ale i k ní musí existovat odpovídající soubor deklarací static.H Zdrojové texty C++ programu přeloží C++ překladač na soubory typu OBJ. Linker k nim připojí potřebné kódy ze statické knihovny static.LIB a odkazy určené dynamic.LIB. Totéž vykoná i pro početné systémové knihovny, na obrázku neuvedené. Nakonec propojí jednotlivé části v jeden pomocný soubor, v příkladu pojmenovaný ~prog.exe, a viz. též Obr. 1-1 na straně 20 . Datové zdroje se překládají odděleně pomocí překladače resource. Ten načte popis dat provedený v souboru prog.RC a z nich vytvoří mezivýsledek prog.RES, který je ve své podstatě jednou velkou datovou tabulkou doplněnou seznamem položek. Na závěr nastupuje program binder. Ten spojí veškeré části aplikace do výsledného souboru prog.EXE a doplní údaje potřebné pro vytvoření procesu, jako velikost zásobníku anebo počáteční rozsah dynamické paměti. Jeho práci řídí jednoduchý textový soubor prog.DEF určující kromě jiného i použitý STUB.EXE, což je pomocný MS-DOS program, který se spouští, pokud 118
je aplikace omylem aktivovaná z MS-DOSu. Ten obvykle vypisuje hlášení, že program vyžaduje Windows. Všechny uvedené operace proběhnout při zadání povelu překladači k přeložení projektu (make nebo build). Chceme-li vyzkoušet výslednou aplikaci, musíme na počítači mít samozřejmě dynamickou knihovnu dynamic.DLL, na níž se odkazujeme z programu. Poznámky: 1. V použitém příkladu existovalo několik souboru prog, které se od sebe lišily příponami. Jednalo se pouze o ukázku. Jména souborů lze volit libovolně za předpokladu, že se dodrží předepsané přípony. 2. Soubor *.RC můžeme editovat buď jako běžný text nebo pomocí speciálních grafických editorů, např. Resource Workshop. Grafické editory dokážou provést také změnu v části resource už hotové aplikace. Jinými slovy, můžeme jimi otevřít libovolnou aplikaci a upravit její datové tabulky. Rovněž tak lze z libovolného programu EXE vzít prvky resource, které se nám líbí, a použít je ve vlastním programu. Samozřejmě nesmíme narušit licenční oprávnění. 3. Soubory *.RC mohou obsahovat příkazy pro vložení vnějších datových souborů, analogie příkazu #include, například pro přidání bitmap, ikon a podobně. Na obrázku tato možnost není graficky znázorněna. 4. Překladače nabízejí pro část knihovních funkcí použitého jazyka jejich připojení buď prostřednictvím statické knihovny nebo dynamické knihovny. Použití dynamické knihovny zkracuje soubor aplikace a urychluje linkování, ale výsledek nelze spustit na počítači, na kterém neexistuje příslušná dynamická knihovna. Statické knihovny naproti tomu vytvářejí sice delší, ale univerzálnější kód. Vhodné je využit dynamické knihovny při vývoji aplikace, ale finální verzi přeložit se statickými knihovnami.
4.8 Soubor aplikace Soubor každé aplikace se skládá z několika dílčích části, které ukazuje Obr. 4-5.
EXE header STUB NEW header Popis prvků aplikace
Kód programu RESOURCE
Hlavička kompatibilní s MS-DOS programy Kód MS-DOS programu vypisujícího obvykle hlášení "Program vyžaduje MS-Windows" . Seznam datových zdrojů aplikace + další informace nutné pro vytvoření procesu
Operace programu Datové zdroje programu, např. ikony, bitové mapy, data pro vytvoření menu, dialogů a pod. Pomocné tabulky připojené překladačem, které slouží pro krokování programu. Windows je ignorují.
Obr. 4-5 Èásti souboru aplikace
119
Na svém začátku má soubor aplikace hlavičku kompatibilní z MS-DOS programy specifikující kód programu spustitelného v režimu MS-DOS. Ten se nazývá STUB a obvykle vypisuje nějakou analogii hlášky - Program vyžaduje MS-Windows. Za kódem části STUB následuje popis prvků souboru aplikace, označovaný jako New Header (nová hlavička). Ten obsahuje seznam datových zdrojů přiložených k aplikace a výčet dynamických knihoven, které program potřebuje ke svému běhu, aby je operační systém mohl nahrát do paměti v době, kdy ze souboru aplikace vytváří proces. Blížeji bude o mechanismu připojování dynamických knihoven pojednáno v kapitole 12. Za blokem New Header se nachází vlastní kód programu včetně globálních a statických dat, za kterým jsou připojené datové zdroje aplikace, označované jako resources. Ty zahrnující data potřebná pro běh programu - jako například ikony, bitové mapy, tvary menu a dialogů. Resources mají pouze pomocný charakter a nemusejí být v aplikaci zahrnuté. Lze je nahrávat i z jiných souborů nebo je dynamicky vytvářet programem. Spojení datových zdrojů a aplikace pouze usnadňuje práci programátorovi a zjednodušuje manipulaci s aplikací. Na konci souboru aplikace se mohou ještě někdy nacházet také pomocné tabulky pro ladění programu, které tam přidal překladač a jejichž délka někdy dosahuje i několika megabytů. !$ Nyní víme již všechny potřebné pojmy a můžeme si vytvořit první program pro Windows.
!$
Překladače Visual C++ 5.0 a Borland C++ Builder dávají ladicí informace do jiných souborů a nepřipojují je k souboru aplikace. Umisťuje je tam například Borland C++. Chceme-li jím vytvořený soubor aplikace podstatně zkrátit, můžeme buď program přeložit s vypnutými informacemi pro ladění, anebo použít s ním dodávaný podpůrný program TDSTR32.EXE, eventuálně TDSTRIP.EXE pro 16-bitové programy, který ladicí informace ze souboru aplikace odstraní.
120
5 Kapitola 5 - Windows API Základní systémové rozhraní Windows se nazývá Windows Application Interface, zkráceně Windows API, a pro Win32 nabízí přes tisíc služeb, k nimž se musí připočíst kolem tří set zpráv a desítek datových struktur pro předávání parametrů. Uvedený počet lze dále rozšířit ještě pomocí dynamických knihoven. Na rozdíl od MS-DOSu, ve Windows API neexistují žádné vstupní body a čísla služeb, analogie INT 21H, ale každá služba se volá jménem jako funkce, a proto se často služby OS Windows často nazývají jenom funkce. O způsobu, jakým se realizuje propojení uživatelského programu s jádrem OS, budeme hovořit v kapitole 9.19.1b. Výhodou Windows API je podobnost základních prvků pro jednotlivé verze Windows. Příslušné rozdíly budou ukázané v kapitole 8.1. Lze však zhruba říct, většina programů pro Windows 3.1 bude pracovat i pod Windows 95/98 a téměř všechny programy pro Windows 95/98 poběží pod Windows NT. Enormní množství nabízených služeb OS si žádá odlišný přístup k studiu Windows než například k matematice nebo k fyzice. Pouhý referenční popis jednotlivých služeb, s velmi stručným výkladem jejich parametrů, zabírá dva tlusté svazky a řada věcí se mění s příchodem nové verze OS. Naučit se je všechny nazpaměť stylem věta - důkaz, to by představovalo pouhou ztrátu času, protože není jisté, že příslušné operace uplatníme dříve, než zmizí v propadlišti pokroku. Navíc, celý referenční manuál existuje ve formě souborů on-line nápověd. Zbývá otázka, jak se naučit programování ve Windows? Odpověď lze shrnout do několika bodů: Třeba bezpečně vědět, jakým postupem se příslušná úloha řeší. Chceme-li ve Windows API vytvořit okno, musíme mít povědomí o tom, že je napřed nutné zaregistrovat jeho třídu a přiřadit mu callback funkci pro obsluhu zpráv, poté vytvořit vlastní okno a tomu poslat pokyn k zobrazení. Pokud nevíme přesný postup, nápovědy nám příliš nepomohou. Způsob použití představuje prioritní znalost a většinou není nutné rozumět všem detailům. Tenhle požadavek leží v přímém protikladu k exaktním vědám, vyžadujícím hlubokou a přesnou znalost, ale v přírodních vědách je běžný. Entomolog nezná každý existující hmyz, ale pouze několik vybraných druhů. Nelze se mu divit, hmyzí říše je hodně bohatá. Ve Windows jsme na tom podobně. Například, chceme-li kreslit do okna, musíme vytvořit device context (DC), o němž budeme hovořit později. Samozřejmě, lze probrat principy DC, ale jejich podrobný výklad by si žádal nejméně padesát stran. Přitom pro nakreslení obrázku nám většinou stačí, když víme, že DC představuje jakousi spojku mezi aplikací a grafickým zařízením, která zprostředkovává kreslení. Pozor, výše uvedenou radu nelze absolutizovat, jinak se staneme pouhými ovladači tlačítek. U některých prvků je vhodné vědět více o jejich pozadí a rozumět jejich činnosti, protože teprve pak dovedeme plně využívat jejich možnosti. Podobné vědomosti tvoří i náplň tohoto skripta. Naučíme se jména nejdůležitějších služeb a zpráv. Ušetříme si tím čas. Pokud chceme naprogramovat nějakou operaci, snáze vyhledáme vhodné funkce, pokud víme, aspoň přibližně, jejich názvy. Nemusíme však znát jejich parametry. Ty si přečteme v nápovědě, včetně jejich významu. A když si nevíme rady, jak je použít, najdeme si jméno služby OS v příkladech odladěných programů. Ty se často vyskytují jako přílohy k překladači nebo se popisují v učebnicích a představují velmi příhodnou pomůcku pro programování. Ve Windows máme právo nevědět. Podobná rada se udílí při výuce asertivity a je na místě i pro Windows. Ty představuje natolik komplikovaný výtvor, že jim nikdo plně nerozumí; troufám si tvrdit, že včetně jejich tvůrců. Budou vždy existovat pro nás neprobádaná Windows území. O tom se přesvědčil i jeden student FEL, který řešil relativně jednoduchý problém a požádal o radu svého kamaráda, profesionálního programátora Windows. Ten jen pokrčil rameny a prohodil, že podobnou operaci ještě nepotřeboval, a proto o ní nic neví. Student si to nakonec musel sám vyhledat v nápovědách. 121
5.1 První aplikace pro Windows Před ukázkou programování ve Windows API je vhodné ještě zodpovědět otázku - má cenu učit se služby OS, když dnes se vše vytváří pomocí objektů, komponent a jiných makro prvků? To je sice pravda, avšak podobná situaci nikterak nesnižuje význam aplikačního rozhraní Windows, pouze ho odsunula na druhé místo. Povědomí o zacházení s ním, všimněte si, že se hovoří o povědomí, a nikoliv o detailních vědomostech, přináší některé výhody: • API představuje primární prostředí, z jehož principů vycházejí veškeré objektové knihovny nebo jiné nadstavby. Ty díky tomu obsahují ekvivalenty k řadě jeho služeb a víme-li něco o API, snáze zvládneme každé programovací prostředí. • On-line nápovědy k API představují velmi objemné balíky informací, z nichž mnohé se nenajdou v stručných manuálech. Kromě toho, nápovědy k API se psaly na základě podkladů dodaných autory Windows a představují spolehlivější zdroje než různé dokumentace napsané podle údajů z druhé ruky (to se vztahuje samozřejmě i na tato skripta). Umět se orientovat v API, nám zpřístupní velmi rozsáhlé a spolehlivé reference o Windows. • Nadstavby nad Windows API nezahrnují veškeré myslitelné operace. Naproti tomu, API umožňuje vytvořit vše, co nabízí OS. V této fázi výkladu se uvádí obvykle příklad minimálního programu pro Windows, který buď nic neprovádí, anebo vypíše nějakou variantu textu Ahoj. Podobnou aplikaci není obtížné sestavit, ale na druhou stranu program, který téměř nic nedělá, těžko přiblíží principy aplikací Windows API. Z tohoto důvodu porušíme zavedenou tradici a ukážeme si GUI aplikaci s následujícími rysy: • • • • •
Aplikace bude mít titulek a vlastní ikonu. Aplikace se bude ovládat z menu anebo funkčními klávesami. Bude možné měnit velikost okna. Ve středu zobrazeného okna bude trvale červený text Støed. Bude možné zapínat a vypínat zobrazení dvou útvarů v okně: - zeleného šrafovaného čtverce; - modré elipsy. • Bude možné měnit jas barev použitých pro kreslení.
Obr. 5-1 Okno se zobrazeným čtvercem a textem Střed
122
Postup řešení bude mít následující kroky: • Kapitola 5.2 - popis datových zdrojů v souborech resource WinAPI.rc a WinAPI.rh. • Kapitola 5.3 - program WinAPI.c složený ze čtyř částí: a) vytvoření hlavního okna a nezbytná smyčka frontových zpráv; b) callback funkce hlavního okna obsluhující zprávy z menu; c) reakce na povely od menu a od akceleračních kláves; d) kreslení okna. • Kapitola 5.4 - definice modulu WinAPI.def. Aplikaci přeložíme a spustíme.
5.2 Resource Naše aplikace potřebuje tři resource - ikonu, menu a tabulku akcelerátorů. K tomu, aby OS věděl, jak tyto prvky vypadají, se musí sestavit jejich popis ve formě předepsaných datových struktur. Ty lze vybudovat různými způsoby, včetně jejich načtení ze vnějšího souboru nebo sestavení při běhu programu pomocí speciálních služeb OS. Pro prvky, která mají pevně definovaný výchozí tvar, bývá nejjednodušší cestou vytvořit je v resource souboru speciálním editorem. Ikonu v editoru jednoduše nakreslíme. Menu a tabulka akcelerátorů představují komplikovanější elementy. Jak bylo řečené v předchozí kapitole, o menu se plně stará operační systém. Vykresluje ho a posílá callback funkci okna zprávy typu WM_COMMAND a identifikační číslo vybrané položky. Každé položce menu musíme tedy přidělit nejen text, ale i její identifikační číslo. To musí být unikátní v rámci jednoho menu. Dvě různá menu mohou používat stejné identifikační čísla. Obvykle se identifikační čísla položek menu přidělují od 100, ale není to však nutné, volba čísel závisí pouze na nás. Tabulka akcelerátorů definuje funkční klávesy, po jejichž stisku bude zaslaná WM_COMMAND !% zpráva, stejná jako při výběru prvku z menu. Menu a tabulku akcelerátorů proto píšeme najednou. Ke každé akcelerační klávese nemusí samozřejmě existovat odpovídající položka menu a naopak. Kromě toho se povoluje, aby akcelerační tabulka obsahovala duplicitní identifikační čísla, takže několik akceleračních kláves může posílat stejný identifikační kód. Není ale přípustná duplicita kláves. Každá klávesa se musí vyskytovat pouze jednou, tzn. každá posílá pouze jedno identifikační číslo. Zvolíme si pro náš příklad následující toto ovládání a identifikační čísla: Skupina Položmenu ka menu
Funkční klávesa Popis funkce
Identifikační číslo položky
Soubor
Konec
Ctrl-X Ctrl-Q
Konec aplikace
CM_KONEC
101
Obrázek
Čtverec
F5
Nakresli čtverec
CM_CTVEREC
102
Elipsa
F6
Nakresli elipsu
CM_ELIPSA
103
Jas+
F7
Zvětšení jasu obrazu
CM_JAS
104
Jas-
F8
Zmenšení jasu obrazu CM_JASMINUS 105
Smazat
Shift-Delete
Smazání obrazu
!%
CM_SMAZAT
106
Informace, jestli k volbě došlo z menu anebo funkční klávesou, je obsažená v pomocném parametru zprávy, který udává tzv. notification kód. O něm budeme hovořit až v kapitole 7.
123
Příkaz Konec uzavře aplikaci. Volby Ètverec a Elipsa zapínají či naopak vypínají zobrazení čtverce, respektivě elipsy v okně. Povely Jas+ a Jas- bode možné upravit intenzitu barvy a poslední volba Smazání obrazu zruší zobrazení a bude odpovídat zrušení obou voleb Ètverec a Elipsa. Vytvoření menu spolu s odpovídajícím přiřazením funkčních kláves je velmi jednoduché. Identifikační popíšeme v include souboru WINAPI.RH, datové zdroje vytvoříme grafickým editorem, který uloží výsledek jako textový soubor WINAPI.RC. /* WINAPI.RH */ #define CM_KONEC 101 #define CM_ELIPSA 102 #define CM_CTVEREC 103 #define CM_SMAZAT 104 #define CM_JAS 105 #define CM_JASMINUS 106 /* WINAPI.RC */ #include "winapi.rh" WINAPI_MENU MENU // WINAPI_MENU je námi přidělený název datovému zdroji MENU { POPUP "&Soubor" // Znak & zajistí podtržení písmene S { MENUITEM "&Konec\tCtrl-X", CM_KONEC // Značka \t zajistí odsazení textu Ctrl-X } POPUP "&Obrázek" { MENUITEM "&Čtvere&c\tF5", CM_CTVEREC MENUITEM "&Elipsa\tF6", CM_ELIPSA MENUITEM SEPARATOR // Příkaz vloží vodorovnou oddělující čáru MENUITEM "&Jas +\tF7", CM_JAS MENUITEM "J&as -\tF8", CM_JASMINUS MENUITEM SEPARATOR MENUITEM "&Smazat\tShift DEL", CM_SMAZAT } } WINAPI_MENU ACCELERATORS // WINAPI_MENU je název pro datový zdroj ACCELERATORS { VK_F5, CM_CTVEREC, VIRTKEY // F5, kód VK_F5 představuje označení překladače VK_F6, CM_ELIPSA, VIRTKEY // F6 VK_DELETE, CM_SMAZAT, VIRTKEY, SHIFT // Shift-Delete VK_X, CM_KONEC, VIRTKEY, CONTROL // Ctrl-X VK_Q, CM_KONEC, VIRTKEY, CONTROL // Ctrl-Q posílá stejné identifikační číslo jako Ctrl-X VK_F7, CM_JAS, VIRTKEY // F7 VK_F8, CM_JASMINUS, VIRTKEY // F8 } WINAPI_ICON ICON "winapi.ico" // WINAPI_ICON typu ICON se vloží z vnějšího souboru winapi.ico
124
Obr. 5-2 Výsledný vzhled jednotlivých položek menu Při použití editoru Resource Workshop vytváříme elementy v interaktivním režimu a výsledek naší práce se uloží jako textový soubor. Menu i tabulku akcelerátorů píšeme současně. Ikony lze popsat v textovém souboru ve formě seznamu hexadecimálních hodnot, specifikujícího barvy jejich jednotlivých bodů, anebo ho zadat pomocí příkazu pro vložení vnějšího binárního souboru ikony, který se při překladu zdrojového textu resource připojí k výslednému souboru resource. Oba způsoby jsou ve svých důsledcích zcela rovnocenné. Pro každý resource element musíme zvolit vhodný název, u něhož se požaduje unikátnost pouze v rámci jednoho typu. Nemohou například existovat dvě ikony se stejným resource jménem, avšak je přípustné, aby se ikona jmenovala stejně jako menu. Ikona "WINAPI_ICON" by se proto mohla pojmenovat "WINAPI_MENU", podobně jako menu a akcelerátory. Shoda jmen u tabulky akcelerátorů a menu není nutná, oba prvky by se mohly jmenovat různě, ale vyžaduje ji vizuální editor, aby poznal, že patří k sobě. Poznámka: Jako jména lze volit i čísla. Některá programovací prostředí to přímo vyžadují, například objektová knihovna MFC, jak bude ukázané v kapitole 6.3. K tomu stačí uvést v souboru *.RH: #define WINAPI_MENU 10 V tom případě by se menu a tabulka akcelerátorů jmenovaly "10".
5.3 Program - WinAPI.C Kód programu se bude vysvětlovat po úsecích - ty budou celkem čtyři a budou tvořit na sebe navazující celky, takže přeložitelný program vznikne pouhým jejich spojením.
5.3a
Vytvoření okna a hlavní smyčka zpráv
Vstupní bodem aplikace je funkce WinMain (blíže viz. strana 113). V té se zpravidla pouze vytváří hlavní okno aplikace, aby OS měl kam zasílat zprávy určené aplikaci, a poté se provádí smyčka frontových zpráv až do konce existence aplikace (viz. strana 111). Vytvoření okna se skládá z následujících kroků: • registrace třídy okna, tj. definice jakéhosi prototypu, od něhož budeme vytvářet jednotlivá okna. Aplikace si může registrovat několik tříd oken, dle své potřeby; • vytvoření okna od zaregistrované třídy. Od zaregistrované třídy lze vytvořit neomezené množství oken; • zobrazení okna pomocí volání systémové služby ShowWindow; • zadání požadavku na vykreslení okna - ten má za následek posláním zprávy WM_PAINT. Důvody pro zavedení tříd a vytváření oken jako jejich objektů leží v prvcích control (tlačítka, list boxy, atd.), které se budou probírat v kapitole 7. Ty vycházejí z předdefinovaných tříd a pro jednotnost se registrace používá pro okna všech typů. Pro běžná okna bývá častou praxí, že se pro každé zaregistruje samostatná třída. (Když to Windows chtějí, tak ať to mají). Ukažme si to celé na příkladu:
125
/* Soubor WinAPI.C - èást 1 ze 4 */ #define STRICT #include <windows.h> #include <windowsx.h> #include "winapi.rh"
// volba přísné kontroly syntaxe - budou se rozlišovat typy handle // vložení základních definic // rozšířené definice // definice identifikačních čísel prvků menu
// deklarace hlavičky callback funkce hlavního okna - ta bude definovaná až v části 2. LRESULT CALLBACK MainWnd (HWND hWnd,UINT message,WPARAM wParam, LPARAM lParam ); #pragma argsused // blokuje varování: "Parameter lpszCmdLine is not used." int PASCAL WinMain ( // vstupní bod programu, viz. strana 113 HINSTANCE hInstance, // instance aplikace HINSTANCE hPrevInstance, // ve Win32 vždy rovné NULL LPSTR lpszCmdLine, // příkazová řádka, která spustila program int nCmdShow // jak zobrazit okno - min., max., skryté, ) { HWND hWnd; // handle hlavního okna HACCEL hAccTable; // handle tabulky akcelerátorů MSG msg; // zpráva if ( !hPrevInstance ) // Ve Win16 aplikace sdílejí zdroje. Test vyloučí dvojí registraci třídy. { // Ve Win32 má každá aplikace svoje vlastní zdroje a test je zbytečný. // Registrace třídy okna -------------------------------------------------------------------------WNDCLASS
wc;
/* Struktura s parametry vlastností třídy okna. Třeba ji vyplnit a předat funkci. Podobný postup se ve Windows používá u více služeb. */
wc.style = CS_HREDRAW | CS_VREDRAW; // překreslit, změní-li hor. a vert. velikost okna wc.lpfnWndProc = MainWnd; // callback funkce okna wc.cbClsExtra = 0; wc.cbWndExtra = 0; // pomocná data, obvykle nepoužívaná wc.hInstance = hInstance; // instance aplikace wc.hIcon = LoadIcon ( hInstance, "WINAPI_ICON"); // ikona z resource aplikace wc.hCursor = LoadCursor ( NULL, IDC_ARROW ); // systémový kurzor šipka wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); // bílé pozadí wc.lpszMenuName = "WINAPI_MENU"; // název menu v souboru resource wc.lpszClassName = "WINPROG"; // jméno pro námi vytvořenou třídu if ( !RegisterClass ( &wc ) ) // registrace třídy službou RegisterClass return ( FALSE ); // Registrace se nezdařila, konec aplikace. Windows nemají } // vnitřní chybový systém. Nehlásí důvody, proč se operace nezdařila. // Vytvoření okna od zaregistrované třídy --------------------------------------------------------------------hWnd = CreateWindow ( "WINPROG", " WINAPI program ", WS_OVERLAPPEDWINDOW, 10, 10, 400, 400, NULL, NULL, hInstance, NULL ); if ( !hWnd ) return ( FALSE ); ShowWindow ( hWnd, nCmdShow ); InvalidateRect(hWnd,NULL,TRUE);
// Služba OS - vytvoř okno // Jméno třídy, libovolné, avšak musí být unikátní. // Titulek okna // styl okna: Minimalizovatelné s rámečkem // x, y pozice levého horního rohu // šířka, výška // není nadřízené okno „parent" // neměníme menu, použijeme to definované při registraci // instance a pointer na pomocná data pro WM_CREATE // Nevytvořilo-li se okno, pak konec aplikace. // Ukaž okno ve stylu zadaném při spuštění aplikace // Celé okno nutno nakreslit, OS pošle WM_PAINT 126
hAccTable = LoadAccelerators(hInstance, "WINAPI_MENU"); // načteme akcelerátory z resource // Věčná smyčka frontových zpráv ------------------------------------------------------------------------------------while ( GetMessage ( &msg, NULL, NULL, NULL ) ) /* čti frontové zpráv, dokud se neobjeví zpráva WM_QUIT */ { if (!TranslateAccelerator(hWnd, hAccTable, &msg)) /* je-li klávesa akcelerátorem, pošli oknu odpovídající zprávu WM_COMMAND */ { TranslateMessage ( &msg ); // stisk a uvolnění klávesy zhustit na zprávu WM_CHAR DispatchMessage ( &msg ); // odešli zprávu callback funkci hlavního okna } } return ( msg.wParam ); // Konec aplikace } /* Konec 1. části WinAPI.C */ Úvodní část programu obsahovala několik bodů, které potřebují podrobnější komentáře. Jméno třídy - jméno pro registrovanou třídu. Bývá zvykem, ale nikoliv nutností, odvodit ho ze jména aplikace (Ve Win16 to zaručovalo nutnou jedinečnost třídy v rámci Windows). Konstanty - v programu se uvádí řada konstant: CS_HREDRAW, CS_VREDRAW, IDC_ARROW, WHITE_BRUSH, WS_OVERLAPPEDWINDOW. Všechny představují čísla definovaná pomocí direktiv #define v souborech windows.h a windowsx.h, podobně jako tisíce dalších obdobných parametrů. Jejich význam se popisuje v nápovědách u jednotlivých funkcí a často se dá odhadnout i z názvu příslušného parametru. Styly - bývají častým parametrem funkcí. Styl představuje obyčejné číslo, většinou 32 bitové, u něhož každý bit nese informaci o jedné vlastnosti. Příslušné kombinace se sestavují pomocí operací bitového logického součtu, jako tomu bylo na řádce wc.style = CS_HREDRAW | CS_VREDRAW; kde se výsledný styl zadává jako kombinace dvou vlastností - překreslit okno při změně horizontální velikosti a při změně vertikální velikosti. Pro některé hodně používané kombinace základních konstant existují i samostatné názvy, viz. další odstavec. Styl okna - uvedený jako parametr WS_OVERLAPPEDWINDOW představuje konstantu, která sdružuje kombinaci šesti parametrů stylu (popsaných v nápovědě pro CreateWindow). Ty určují, že okno má: WS_BORDER - rámeček WS_CAPTION - nahoře titulek, za nějž ho lze okno myší přemístit WS_SYSMENU - ikonu systémového menu vlevo od titulku a zavírací x tlačítko na pravém konci titulku WS_THICKFRAME - silný rámeček, za který lze vzít myší a měnit velikost okna WS_MINIMIZEBOX - minimalizační tlačítko na pravém konci titulku WS_MAXIMIZEBOX. - maximalizační tlačítko na pravém konci titulku Parametrů pro styl okna je 32, ale nečekejte, že všech 2 32 kombinací bude existovat. Pokud zadáme nějakou rozporuplnou (třeba okno bez titulku, avšak se systémovým menu) dojde v nejlepším případě k opravě chyby (vytvoří se automaticky titulek), v tom horším k nevytvoření okna anebo zhroucení celé aplikace. Windows neošetřují chyby a předpokládají, že se na nich spouštějí pouze dokonalé aplikace. Nahrání resource - pro práci s resource, které jsou připojená k souboru aplikace, musíme vědět pouze handle instance aplikace, předávaný jako parametr ve WinMain (hInstance). V příkladu se četly dva resource - ikona a akcelerátory. Všechny funkce pro načtení resource začínají Load... a vracejí handle žádaného prvku. Požadovaná data se však nahrávají ze souboru aplikace teprve v okamžiku, kdy je OS skutečně fyzicky potřebuje.
127
Zde leží hlavní potíž práce s resource - zadáme-li LoadIcon špatné parametry, chyba se neprojeví při jejím volání, ale až mnohem později, aniž Windows sdělí její pravou příčinu. Na druhou stranu, resource nabízejí snadnou dostupnost a velkou efektivitu práce. Potřebuje-li OS místo v paměti, sám z ní odstraní nepotřebná resource a ty při novém použití automaticky nahraje ze souborů aplikací (blíže se o tom bude psát v kapitole 9.1). Všechna resource jsou vedená na aplikaci (a to dokonce i ve Win16) a po jejím skončení se automaticky ruší. Použití systémových zdrojů - Windows v sobě zahrnují řadu přednastavených prvků, které aplikace mohou využívat. Uvedeme-li při nahrávání ikony nebo kursoru místo handle instance konstantu NULL, znamená to, že požadujeme prvek definovaný v OS. Vhodný prvek si pak vybereme předdefinovanou konstantou. Ty se popisují u příslušné funkce. Např.: LoadCursor ( NULL, IDC_ARROW ); // systémový kurzor tvaru šipka LoadIcon ( NULL, IDI_APPLICATION); // systémová ikona obecné aplikace GetStockObject - funkce vrací handle systémového objektu pro kreslení. V naší ukázce se jedná o štětec bílého pozadí okna. O těchto prvcích budeme podrobněji mluvit ještě později. Prozatím můžeme uvést, že pro kreslení do okna se používají dva základní elementy - pero (pen), to dovoluje kreslit čáry, a štětec (brush), který slouží k výplni ploch. InvalidateRectangle - specifikuje plochu okna vyžadující překreslení. Jejím prvním argumentem je handle okna, druhým pointer na souřadnice plochy; je-li NULL, znamená to celé okno. Poslední argument určuje, má-li OS zadanou plochu smazat - true znamená ano, false ne. OS pošle sám oknu WM_PAINT zprávu v okamžiku, kdy určená část okna bude viditelná.
5.3b
Callback funkce okna
Zprávy určené oknu se posílají zaregistrované funkci (window procedure), kterou budeme označovat jako callback funkce okna. Ta musí být typu callback, aby ji OS mohl volat (viz. kapitola 4.4 na straně 106). Definuje se při registraci třídy okna, což znamená, že jedna callback funkce okna zpracovává zprávy všech oken vytvořených od jedné třídy. Kvůli rozlišení, o které okno se jedná, se callback funkci předává v parametru hWnd určujícím handle okna. Jak bylo již řečeno v předchozí kapitole, pro každé okno se zpravidla registruje vlastní třída, a proto callback funkce okna obsluhují většinou jediné okno. Identifikační číslo zprávy udává message. S tou souvisejí i další dva parametry - wParam a lParam. Ve Win32 mají oba délku 32 bitů a označení prvého z nich jako wParam se kvůli zpětné kompatibilitě převzalo z Win16, v nichž má wParam 16 bitů a odpovídá typu WORD. Callback funkce okna obsahuje nejčastěji příkaz switch, který vyvolává vhodné operace na základě přijatých zpráv. Výsledek zpracování zprávy se hlásí příkazem return, s parametrem podle typu zprávy. Obvykle return 0L; značí, že zpráva byla obsloužena bez chyby. Zpráv je ve Win32 přes 300. Zde se zmíníme pouze o těch použitých v příkladu: WM_CREATE bude zaslaná callback funkci okna jako první ze všech zpráv, během provádění CreateWindow, v okamžiku, kdy OS už vytvořil okno, avšak ještě ho nezobrazil. Návratová hodnota 0L udává, že lze pokračovat v tvorbě okna, zatímco return -1L by ukončila tvorbu okna, v důsledku čehož by CreateWindow vrátila NULL - tj. okna nevytvořeno. WM_SHOWWINDOW specifikuje, že v následujícím okamžiku dojde ke změně viditelnosti okna. Její parametr wParam má význam logické hodnoty a nabývá true, pokud okno bude zobrazené, v opačném případě se rovná false, tj. 0. Druhý parametr, lParam, nese informaci o stavu rodičovského okna.V našem příkladu zprávu vyvolalo ShowWindow. Jinak k ní dochází nejčastěji při minimalizaci okna na ikonu nebo naopak roztažení ikony na okno. WM_COMMAND vyvolá řada akcí, v našem příkladu povely menu a akceleračních kláves. Parametr wParam (ve Win32 dlouhý 32 bitů!) specifikuje v dolním slově identifikační číslo prvku, který zprávu zaslal, a v horním slově kód oznámení, tzv. notification code, o němž
128
budeme více hovořit u prvků typu control. Notification code je pro menu vždy rovný 0 a pro akcelerátory 1, podle čehož lze rozlišit povely od menu a od akceleračních kláves. Pozor, formát obou parametrů zprávy WM_COMMAND je odlišný ve Win16, protože tam má wParam jen 16 bitů. Programy pro obě prostředí musejí s touto skutečností počítat. WM_PAINT zašle OS, kdykoliv se musí znovu nakreslit obsah okna, buď celý nebo jeho část. Dále ji lze vyvolat z programu, třeba již zmíněnou funkcí InvalidateRect. WM_DESTROY dostane okno těsně před svým zrušením - pošle se mu voláním DestroyWindow. V našem programu se při WM_DESTROY provede PostQuitMessage(0), čímž se pošle další zpráva WM_QUIT s parametrem udávajícím zadaný 0 exit kódu programu (wParam = 0). Zpráva WM_QUIT způsobí, že funkce GetMessage vrátí 0, čímž se ukončí smyčka frontových zpráv ve WinMain, viz. část 1 WinAPI.C a rovněž také kapitola 4.54.5c na straně 110.
/* Soubor WinAPI.C - èást 2 ze 4. */ // Deklarace hlaviček funkcí, volaných příkazy z menu. Jejich definice budou v části 3. void CmCtverec( HWND hWnd ); void CmElipsa( HWND hWnd ); void CmJasPlus( HWND hWnd ); void CmJasMinus( HWND hWnd ); void CmSmazat( HWND hWnd ); void EvPaint( HWND hWnd ); // Hlavička funkce volané při zprávě WM_PAINT. Blíže viz. část 4. LRESULT CALLBACK MainWnd ( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch ( message ) { case WM_CREATE: return 0L; // Zpráva zaslaná při CreateWindows case WM_SHOWWINDOW: return 0L; // Zpráva zaslaná v důsledku ShowWindow case WM_COMMAND: // Zprávy zaslaná od menu a akceleračních kláves switch (LOWORD(wParam)) // dolní slovo identifikační číslo prvku { case CM_ELIPSA: CmElipsa(hWnd); return 0L; case CM_CTVEREC: CmCtverec(hWnd); return 0L; case CM_JAS: CmJasPlus(hWnd); return 0L; case CM_JASMINUS: CmJasMinus(hWnd); return 0L; case CM_SMAZAT: CmSmazat(hWnd); return 0L; case CM_KONEC: DestroyWindow(hWnd); // zruš okna+ pošli zprávu WM_DESTROY return 0L; } case WM_PAINT: // Zpráva zaslaná OS, je-li nutné překreslit celé okno nebo jeho část EvPaint(hWnd); // Překresli okno return 0L; case WM_DESTROY: // Poslední zpráva, kterou okno dostane před svým zrušením PostQuitMessage ( 0 ); // pošli aplikaci zprávu WM_QUIT return 0L; default: // Zprávy, které neobsluhujeme, nezahazujeme, ale vracíme systému! return ( DefWindowProc ( hWnd, message, wParam, lParam ) ); // Základní obsluha zpráv } // konec příkazu switch(message) } /* Konec 2. části WinAPI.C */
129
Tok zpráv při vytváření okna, část 1 a 2 programu WinAPI.c, ukazuje obrázek: struct WNDCLASS wc;
Zadej popis vlastností okna
RegisterClass(&wc);
Registruj okno Vytvoř okno
HWND hWnd = CreateWindow(...);
Ukaž okno
ShowWindow ( hWnd, cmdShow );
Pošli WM_PAINT
InvalidateRect(hWnd, NULL, true);
hWnd
Message==WM_CREATE
WM_CREATE WM_SHOWWINDOW
return 0L; // =OK | = -1 chyba
WM_PAINT
Message==WM_SHOWWINDOW Message==WM_PAINT EvPaint(); return 0L;
WM_COMMAND Wpar= ident. prvku
Message==WM_COMMAND
File
Wpar==CM_JAS
Kreslení F6 Elipsa
Wpar==CM_EXIT
Jas
DestroyWindow(hWnd); return 0L;
F7
WM_DESTROY
Wpar==CM_ELIPSA
CmElipsa(); return 0L;
W i n d o w s
MainWnd(hWnd,Message,Wpar, Lpar)
Zrušení okna
Změna vzhledu položky menu Elipsa Konec smyčky frontových zpráv a tím i celé aplikace
Message==WM_DESTROY
WM_QUIT
PostQuitMessage(0); return 0L; Obr. 5-3 Tok zpráv pøi vytváøení a rušení okna
Jak vidíte, činnost Windows se opírá o zprávy, ty zase o další zprávy. OS zasílá zprávy při výběru položek z menu, při stisku klávesy, při volání systémových služeb, při pohybu myší a tak dále. Na mnoho zpráv se reaguje posláním dalších zpráv. Umět myslet ve zprávách, to patří k předpokladům programování ve Windows. Na principu zpráv dnes pracuje většina prostředí, používajících několik nezávislých vstupů povelů, jako třeba menu, tlačítka, klávesy a podobně. Na zprávách stojí i X-Windows pro 130
UNIX nebo OS/2 a rovněž i stařičká textová okna TurboVision, dodnes používaná aplikace pro MS-DOS. Svým způsobem jsou na zprávách vystavěné i datové sítě. Zprávy a okna nejsou tedy ani vynálezem Windows a ani jejich výlučnou doménou. Windows pouze využívají jejich obecné principy. Callback funkce okna slouží především jako výhybka, která rozesílá přijaté zprávy obslužným funkcím. Kromě toho se vytvoření okna nebo zrušení okna opírá o pevně definované postupy, který lze měnit pouze volbou parametrů. Drtivá většina kódu se opakuje u každého okna a při psaní ve Windows API se obvykle kopírují.celé bloky programu. Tato skutečnost podpořila vznik objektových knihoven.
5.3c
Příkazy
Funkce, realizují jednotlivé příkazy a volané z callback funkce okna, nekreslí přímo na plochu okna, ale pouze mění hodnoty globálních proměnných, v nichž se uchovávají informace o aktuálním vzhledu obrazu. Aktualizace okna se zařídí pomocí InvalidateRectangle, jejíž zavolání má za následek poslání zprávy WM_PAINT příslušnému oknu, buď ihned, je-li právě viditelné, nebo až v okamžiku, kdy se jeho plocha či její část stane viditelnou. Zpráva WM_PAINT se obslouží ve funkce EvPaint, která nakreslí okno, jak bude ukázáno v WinAPI.C části 4. Uvedený mechanismus zaručuje existenci informace o aktuálním stavu okna, takže ho lze kdykoliv obnovit. Musíme mít na paměti, že plocha okna může být v kterémkoliv časovém okamžiku použitá OS pro jiný prvek, třeba pro okno jiné aplikace. Po opětovném zviditelnění původního okna, nebo jeho části, mu OS pošle zprávu WM_PAINT. Funkci EvPaint nelze volat přímo a musí se použít InvalidateRectangle, protože při výskytu WM_PAINT se pro kreslení používá jiný DC (device context - jemu se budeme věnovat v části 4 WinAPI.C) a pouze OS může rozhodnout o okamžiku, kdy se toto DC uplatní. Zapnutí nebo vypnutí zobrazení čtverce, respektivě elipsy, budeme indikovat zatržením příslušné položky v menu. O menu se plně stará OS, který si i sám uchovává informace o jeho aktuálním vzhledu. Voláním příslušných systémových služeb můžeme ovlivnit jeho vzhled. Použijeme k tomu CheckMenuItem, měnící stav zaškrtnutí položky menu. To se zadá parametrem atribut. Jeho bity mají význam: • MF_BYCOMMAND, specifikující, že položka menu je určená svým identifikačním číslem. Druhou možností, zde nepoužitou, by bylo zadání MF_BYPOSITION relativní pozice položky menu. • MF_CHECKED nebo MF_UNCHECKED, definující stav zatržení.
/* Soubor WinAPI.C - èást 3 ze 4. */ BOOL ctverec = FALSE; BOOL elipsa = FALSE; BYTE jasbarvy = 128;
// zobrazený čtverec // zobrazená elipsa // jas barvy 0..255
void CheckMenu(
// pomocná funkce pro změnu stavu zatržení u položky menu HWND hWnd, // handle okna, jemuž patří menu int id, // identifikátor prvku menu BOOL oznaceno // stav zatržení ) { UINT atribut; if(oznaceno) atribut = MF_BYCOMMAND | MF_CHECKED; else atribut = MF_BYCOMMAND | MF_UNCHECKED; CheckMenuItem( // Služba OS - změna stavu označení položky menu GetMenu(hWnd), // Získání handle menu z handle okna id, // Identifikační číslo položky atribut ); // Atribut položky definující stav označení a význam id } 131
void CmCtverec( HWND hWnd ) // Zobrazení nebo smazaní čtverce { ctverec = !ctverec; CheckMenu(hWnd, CM_CTVEREC, ctverec); InvalidateRect(hWnd,NULL,TRUE); }; void CmElipsa( HWND hWnd ) // Zobrazení nebo smazání elipsy { elipsa = !elipsa; CheckMenu(hWnd, CM_ELIPSA, elipsa); InvalidateRect(hWnd,NULL,TRUE); }; void CmJasPlus( HWND hWnd ) // Zvýšení jasu barvy { if(jasbarvy<240) jasbarvy += 16; else jasbarvy=255; InvalidateRect(hWnd,NULL,TRUE); }; void CmJasMinus( HWND hWnd ) // Snížení jasu barvy { if(jasbarvy>=16) jasbarvy-=16; else jasbarvy = 0; InvalidateRect(hWnd,NULL,TRUE); }; void CmSmazat( HWND hWnd ) { ctverec=elipsa=FALSE; jasbarvy=128; // Vynulování proměnných s informací o stavu obrazu CheckMenu(hWnd, CM_ELIPSA, elipsa); CheckMenu(hWnd, CM_CTVEREC, ctverec); InvalidateRect(hWnd,NULL,TRUE); } /* Konec 3. části WinAPI.C */ Příkazy v části 3 reprezentují první skutečně výkonnou část programu, zatímco předchozí části prováděly pouze operace nutné pro vlastní správu grafického prostředí Windows. Jak uvidíme dále, část 3 nebude prakticky záviset na použitém programovacím prostředí. Jinými slovy - budeme-li vytvářet vlastní aplikaci, nic za nás výše uvedené příkazy nenapíše.
5.3d
Kreslení okna a DC
Funkce EvPaint, obsluhující událost WM_PAINT, se volá se v okamžiku, kdy se obnovuje obsah okna. Na displej se přistupuje prostřednictvím DC, device context. DC se musí před kreslením vytvořit a po něm opět vrátit OS, protože představuje náročný prvek a jeho nevrácení by blo!& kovalo výkonnost OS. DC má význam přístupové brány do GDI, Graphics Device Interface, tj. speciální knihovny Windows, zprostředkovávající dostup na grafická zařízení pomocí instalovaných driverů.
Aplikace
Device context
Device driver
Výstupní zařízení
G D I Obr. 5-4 GDI a Device Context
!&
Ve Win16 představuje nevracení DC dokonce přímo vražednou situaci. Existuje v nich totiž pouze pět DC pro grafické výstupy a jejich vyčerpaní zablokuje celý OS.
132
Aplikace zadává povely ve tvaru nezávislém na konkrétním zařízení, tzv. device independent graphics, a GDI je transformuje do vhodné formy závisející na fyzických možnostech zařízení, tzv. device dependent graphics. Atributy této transformace uchovává DC, device context. Je-li zadán požadavek na vytvoření DC, GDI zváží následující charakteristiky zařízení: • • • • • • • •
provedení ovladače zařízení; fyzickou velikost použitelné plochy pro výstup; barevné možnosti výstupu; barevnou paletu zařízení; objekty, které dovoluje hardware zařízení kreslit; vestavěné možnosti limitace okraje kreslicí plochy, tzv. clipping; vestavěné možnosti kreslení křivek, polygonů a podobně; vestavěné možnosti psaní textu.
Na základě těchto charakteristik se vytvoří odpovídající DC tak, aby se optimálně využívaly možnosti periférie. Umožňuje-li například grafická karta kreslit úsečky, pak se tyto povely posílají přímo na ni, zatímco v opačném případě GDI rýsuje úsečky bod po bodu. GDI šetří práci aplikacím, které by se jinak samy musely starat o vlastnosti periferií, podobně jako aplikace pro MS-DOS. Kromě toho GDI sjednocuje rozhraní. Jeden kreslící algoritmus lze použít jak pro obrazovku, tak pro jehličkovou tiskárnu, samozřejmě s hodně odlišnou kvalitou výsledku. Pro událost WM_PAINT se vyžaduje vytvoření zvláštního DC, pomocí služby BeginPaint. Ta současně omezuje kreslení na určený obdélník, tzv. clipping. Překreslovaná oblast byla definovaná voláním InvalidateRectange, buď jedním nebo několika postupnými, v tom případě se jednotlivé obdélníky slučují. Dále obdélník pro překreslení nastavuje i OS, pokud nějakou část okna překryl jiným prvkem. Program, který obsluhuje událost WM_PAINT, může pokaždé vytvářet celý obraz, jakoby plocha okna byla úplně smazaná. GDI z celé kresby vybere pouze části, které leží uvnitř oblasti zadané pro překreslení, a ty odešle na výstupní zařízení. Ostatní ignoruje. V důsledku toho probíhá překreslování okna velmi rychle, přestože na fyzickou periferii se přistupuje jen v nezbytně nutné míře. DC vytvořené BeginPaint lze používat výhradně během obsluhy WM_PAINT a jeho vytvoření v jiných situacích může vést k nestandardnímu chování systému. Pro kresby mimo WM_PAINT se používají jiné DC, vytvořené například GetDC, GetWindowDC, avšak jejich uplatnění pro obsluhu WM_PAINT se nedoporučuje. Nutnost vytvářet odlišná DC neblokuje tvorbu univerzálních funkcí pro kreslení. Každé DC je totiž na úrovni programu reprezentováno pomocí svého handle a to představuje 32 bitové číslo (ve Win32). Lze proto vytvářet kreslicí funkce zcela nezávislé na právě používaném DC a těm předávat potřebné handle DC jako jejich argument. Každý DC se po použití musí opět zrušit; DC vytvořený BeginPaint se uvolňuje EndPaint. Totéž platí i pro grafické prvky vytvořené na základě DC. Ve WinAPI.C se používá pero (pen), kterým se kreslí čáry, a štětec (brush), určený pro výplně ploch. Oba objekty se ruší DeleteObject. Naproti tomu se neruší barva kreslení zadávaná makrem RGB. Ta je číslem typu long.
/* Soubor WinAPI.C - èást 4 ze 4. */ void EvPaint(HWND hWnd) // Obsluha události WM_PAINT { PAINTSTRUCT ps; // struktura, do níž OS uloží souřadnice oblasti pro překreslení COLORREF barva; // 32 bitové číslo udávající barvu HDC hDC; // handle pro DC HBRUSH hbrush; // handle štětce pro výplně ploch HPEN hpen; // handle pera pro kreslení čar char * text="Střed"; // řetězec, který se vypíše ve středu okna 133
RECT int
rect; // struktura pro uložení souřadnic obdélníku xs, ys; // souřadnice středu okna
GetClientRect(hWnd, &rect); /* relativní souřadnice plochy okna, použitelné pro kreslení, vztažené k oknu: - levý horní roh rect.left, rect.top = 0, 0 - pravý dolní roh rect.right, rect.bottom */ xs=(rect.left+rect.right)/2; ys=(rect.top+rect.bottom)/2; // vypočteme souřadnice středu okna hDC = BeginPaint ( hWnd, &ps );
// získáme DC pro obsluhu zprávy WM_PAINT
SetBkMode(hDC, TRANSPARENT);
// nastavíme transparentní kresbu
if(elipsa) { barva = RGB(0, 0, jasbarvy); hpen = CreatePen(PS_SOLID, 1, barva ); SelectObject(hDC,hpen); hbrush = CreateSolidBrush(barva); SelectObject(hDC, hbrush);
// modrá barva zadané intenzity // modré pero o síle 1 // Následující příkazy budou kreslit modré čáry // Vytvoření modrého plného štětce. /* Zadáme štětec, který následující příkazy budou používat pro výplně ploch. */ Ellipse(hDC, rect.left, rect. top, rect.right, rect.bottom); /* Nakreslíme elipsu modrým perem a tu vyplníme modrým štětcem */ DeleteObject(hbrush); DeleteObject(hpen); // Uvolníme štětec a pero
} if(ctverec) { barva = RGB(0, jasbarvy, 0); // zelená barva zadané intenzity hpen = CreatePen(PS_SOLID, 3, barva ); // zelené pero o síle 3 hbrush = CreateHatchBrush(HS_DIAGCROSS, barva); // štětec pro zelené křížové šrafování SelectObject(hDC,hpen); // Následující příkazy budou kreslit modré čáry SelectObject(hDC,hbrush); // Následující příkazy šrafují Rectangle(hDC, xs-100, ys-100, xs+100, ys+100); /* Nakreslíme čtverec zeleným perem a ten vyplníme zeleným křížovým šrafováním */ DeleteObject(hbrush); DeleteObject(hpen); // Uvolníme štětec a pero } barva = RGB( jasbarvy, 0, 0); // červená barva zadané intenzity SetTextColor(hDC, barva ); // barva textu SetTextAlign(hDC, TA_CENTER | TA_BASELINE); /* Umístěný vůči referenčnímu bodu, zarovnat vodorovně na jeho střed a svisle na základní linku písma */ TextOut ( // napsat text předem zadanou barvou hDC, // handle DC xs, ys, // referenční bod, vůči němuž bude vztažená poloha textu (LPSTR) text, // psaný text lstrlen(text) /* délka textu - lstrlen je vestavěná funkce Windows, přímý ekvivalent strlen knihovní funkce jazyka C. Lze používat obě funkce.*/ ); EndPaint ( hWnd, &ps ); }
// Uvolníme vytvořený DC pro kreslení; /* Konec 4. a poslední části WinAPI.C */
Kreslení obrazu v okně představuje další výkonnou část programu. Přestože ji vždy také musíme napsat, na rozdíl od části 3 se bude více měnit, protože řadu operací lze zjednodušit pomocí předdefinovaných objektů.
134
Poznámky: • Objektové knihovny dovolují většinou kreslit i službami Windows API, neboť ony samy musejí používat. K tomu si stačí od příslušných objektů vyžádat handle DC a poté by se daly použít všechny příkazy s ním spojené. Mělo by to nějakou výhodu? Občas ano - dají se tak vytvořit operace, které v příslušné knihovně chybějí, i když je Windows umějí. • Funkce TextOut psala přednastaveným systémovým fontem. Vytvoření jiného fontu by na úrovni Windows API bylo příliš složité, protože se příslušné službě OS musí předat 14 parametrů. Změnu fontu si proto necháme až na práci s objekty, kde v tom nejjednodušším případě stačí zadat jméno a velikost požadovaného fontu. • V ukázce chybělo nastavení barvy pozadí (služba SetBkColor), neboť se kreslilo v transparentním módu. Pokud by se zadalo SetBkMode(hDC, OPAQUE); pak by se kreslilo neprůhledně. Předchozí kresby by neprosvítaly, znaky písma a šrafování by se vytvářelo i s pozadím. Definice vhodné barvy pozadí by pak byla nezbytná. Módy kresby, transparentní a neprůhledný, lze přepínat kdykoliv.
5.4 Definice modulu Programy pro Win32 většinou nepotřebují soubor definice modulu, Module Definition, jako aplikace pro Win16, protože většinu podmínek v něm obsažených realizuje jejich linker. Výjimkou bývá tvorba dynamických knihoven, o níž se zmíníme později. Nicméně pro úplnost můžeme uvést jednoduchý DEF soubor, s nímž vystačíme pro většinu Win32 aplikací. NAME WINAPI DESCRIPTION 'Ukázková úloha pro Windows API' Jednotlivé položky mají následující význam: NAME - určuje jméno výsledného souboru aplikace. Doporučuje se odpovídalo souboru aplikace bez přípony EXE. DESCRIPTION - libovolný text stručně charakterizující program. Bude vložený do souboru aplikace. Pozor, text je ohraničený jednoduchými uvozovkami! Pokud si přejeme používat jiný program STUB, který se vykoná, když dojde ke spuštění aplikace v MS-DOSu (viz. kapitola 4.8 na straně 119), pak lze připojit ještě řádek: STUB 'WINSTUB.EXE' definující jméno příslušného MS-DOS programu. Zde je použitý systémový program, který vypíše hlášení, že aplikace potřebuje prostředí Win32. Poznámka: Win16 vyžadují v DEF souboru ještě další položky, zejména určení vlastností segmentů (příkazy CODE, DATA, SEGMENTS) a velikosti zásobníku (příkaz STACKSIZE). Ve Win32 se ale žádné segmenty nepoužívají a zásobník nastavuje v linkeru, přičemž mapování paměti dovoluje pro něj použít velké hodnoty (obvykle řádu megabytů), s nimž vystačí většina programů. Pokud ne, zásobník lze prodloužit. Adresový prostor Win32 aplikace zahrnuje 4 GB virtuální paměti.
135
6 - Objektový program pro Windows Prostředí Windows API se nehodí k přímému psaní programů. Představuje pouhé služby OS, jejichž použití vyžaduje dlouhé úseky povinného zdrojového kódu pro obsluhu grafického prostředí, jako třeba pro tvorbu okna anebo pro smyčku frontových zpráv. Kvůli tomu vznikla řada nadstaveb, majících většinou charakter objektových knihoven, nad nimiž pracují vizuální programovací metody, dovolující vytvářet vzhled aplikace interaktivně. Objektový přístup umožňuje snadno vytvářet opakující se části programu. Nejčastější rysy jednotlivých prvků se vytvoří ve formě tříd, které se uspořádají do knihovny. Třídy se pak používají k tvorbě grafických objektů buď přímo, pokud jimi popsaný tvar a chování objektu vyhovuje, nebo od nich odvozují jiné třídy metodami dědičnosti, jejíž charakter se modifikuje do požadovaného tvaru. Místo celého zdrojového kódu programu se tedy píší pouze jeho odlišnosti vůči výchozímu standardu definovanému příslušnými třídami objektové knihovny. Na ukázku si vybereme knihovnu OWL, Object Windows Library, překladačů Borland C++. Ta sice nepatří mezi nejpoužívanější objektový nástroj, ale zato disponuje jinou významnější vlastností - nabízí dokonalou ryze objektovou strukturu. Její návrháři Carl Quinn a Bruneau Barbet odvedli mistrovskou práci a vytvořili dílo, které nejen vychází z pravidel objektového programování, ale navíc je umí i dokonale využívat. Věčná škoda, že OWL prohrála v soutěži o nejpoužívanější objektový nástroj pro důvody, které se velmi podobají těm, pro něž neuspěl LINUX a OS/2 při závodu s Windows, blíže se o nich v kapitole 6.3. I když je v OWL v defenzívě, přesto poslouží na demonstraci možností objektů a také jako inspirace pro vytváření vlastních programů. Napsat v ní jednoduchou aplikaci není nic těžkého. Vypadá takto: #include // Vložení deklarací třídy TApplication int OwlMain(int argc, char* []) // Vstupní bod programu založeného na OWL { TApplication mujprogram; // Vytvoření objektu mujprogram int exitcodel; // Exit kód programu = parametr zprávy WM_QUIT exitcode = mujprogram.Run(); // Volání metody Run deklarované ve třídě TApplication return exitcode; // Vracíme exit kód jako výsledek aplikace } Na začátku programu se vkládají deklarace třídy TApplication, základního prvku všech programů založených na knihovně OWL. TApplication umí inicializovat aplikaci, vytvořit hlavní okno aplikace a poté provádět smyčku frontových zpráv. Vstupním bodem programu je nyní OwlMain. Funkce WinMain, výchozí bod všech aplikací pro Windows, je využívaná objektovou knihovnou pro její inicializaci. Během ní se uloží hodnoty parametrů WinMain do interních proměnných, aby byly neustále k dispozici, a dekóduje se příkazová řádka do tvaru odpovídajícímu funkci main, tj. na jednotlivé dílčí argumenty. Jakmile proběhne inicializace, objektová knihovna zavolá OwlMain, kde začíná vlastní uživatelův program. Ten vytvoří lokální objekt mujprogram, typu TApplication, obsahující datové členy třídy TApplication, přičemž se automaticky zavolá konstruktor TApplication(), aby je inicializoval. Grafické okno vznikne až v metodě Run. Ta provede registraci třídy okna, vytvoření okna a jeho zobrazení a poté provádí frontovou smyčku zpráv až do přijetí zprávy WM_QUIT. Exit kód poslaný touto zprávou bude návratovou hodnotou metody Run a současně, díky příkazu return, i výsledkem OwlMain, což znamená exit kódem celé aplikace. Objekt mujprogram má význam pouze pro zavolání metody Run, a proto lze program zkrátit dočasným objektem (viz. kapitola 3.3 na straně 67). Může se přitom vynechat i definice exitcode a dočasný objekt použít přímo v příkazu return.
136
Další, pouze kosmetickou úpravu, provedeme uzavřením identifikátorů formálních parametrů argc a argv do komentářových závorek, aby se potlačilo varování překladače, že nejsou ve funkci OwlMain použité. Výsledný zdrojový kód se nám zmenší na již běžně používaný zápis: #include // Vložení deklarací třídy TApplication int OwlMain( int /*argc*/, char * /*argv*/ [ ] ) // OwlMain pouze s typy formálních parametrů { return TApplication().Run(); } // Metoda Run aplikovaná na dočasný objekt Uvedený program představuje nejmenší OWL aplikaci. Po překladu a spuštění vytvoří prázdné hlavní okno programu s ikonou systémového menu, které ho dovoluje zavřít.
6.1 Převedení WinAPI do programu v OWL Aplikace vytvořená TApplication není bez úprav použitelná a třeba ji modifikovat, zejména její hlavní okno. K tomu se od TApplication musí odvodit nová třída, v níž se předefinuje virtuální metoda TApplication::InitMainWindow, která se volá z metody Run a provádí inicializaci hlavního okna. OWL zahrnuje několik různých tříd vhodných pro vytvoření oken. Pro TAplication se vyžaduje třída TFrameWindow, obsahující podporu menu. Konstruktoru TFrameWindow se předává handle rodičovského okna - ten je pro náš případ rovný 0, neboť vytváříme hlavní okno aplikace a to žádné nadřízené okno nemá. Rovněž lze v konstruktoru uvést titulek okna a pointer na podřízené okno, tzv. klienta, které zatím nebudeme definovat a vrátíme se k němu později. Objekt třídy TFrameWindow vytvoříme operátorem new jako dynamický, aby doba jeho existence (duration, viz. kapitola 1.21.2c na str. 16) přesahovala dobu provádění metody. O zrušení dynamického objektu se postará destruktor TAplication. Získaný pointer využijeme pro přiřazení ikony, menu a tabulky akcelerátorů. Ty už existují v souboru resource (WinAPI.rc), popsaném v minulé kapitole, který lze beze změny použít. Stačí ho jen přidat do projektu překladače. #include #include
// vložení deklarací třídy TApplication // vložení deklarací třídy TFrameWindow
class MojeAplikace : public TApplication // odvození nové třídy { public: MojeAplikace() : TApplication() {} // nutné volání konstruktoru základní třídy virtual void InitMainWindow() // nahrazení virtuální metody naším kódem { TFrameWindow * pwnd; // pointer na prvek třídy TFrameWindow pwnd = new TFrameWindow( // vytvoření dynamického objektu 0, // handle nadřízeného okna - není " OWL aplikace", // titulek okna 0 // podřízené okno, tzv. klient, zatím není ); pwnd->SetIcon(this, "WINAPI_ICON"); // načteme ikonu ze souboru této aplikace pwnd->Attr.AccelTable = "WINAPI_MENU"; // určíme akcelerační tabulku pwnd->Attr.X=10; pwnd->Attr.Y=10; pwnd->Attr.H=400; pwnd->Attr.W=400; // umístění okna pwnd->AssignMenu("WINAPI_MENU"); // oknu přidáme menu SetMainWindow(pwnd); // určení hlavního okna aplikace } }; int OwlMain( int /*argc*/, char * /*argv*/ [] ) // OwlMain pouze s typy formálních parametrů { return MojeAplikace().Run(); } // Metoda Run aplikovaná na dočasný objekt
137
Ikona se k oknu přiřadila metodou SetIcon, definovanou v TFrameWindow: bool SetIcon(TModule* iconModule, TResId iconResId); kde první parametr udává pointer na třídu TModule, určující zdroj, z něhož se ikona nahrává, což dovoluje používat i ikony uložené v jiných aplikací nebo v dynamických knihovnách. Třída TModule se v OWL stará o práci s resource a tvoří základní třídu TApplication a tedy i základní třídu MojeAplikace. Jinými slovy - MojeAplikace obsahuje všechny metody a data deklarované v TModule a v TApplication. Pointer this ve volání SetIcon ukazoval na objekt třídy MojeAplikace (tzn. na právě prováděnou aplikaci) a tím i na TModul, protože redukci typu pointru na některou základní třídu je možné vždy provést, viz. Obr. 3-1 na straně 72. Druhý parametr metody SetIcon označuje objekt typu TResId, jednoduchou podpůrnou třídu, která má různé konstruktory, např.: TResId(const char far * resString); TResId(int resNum); Přetížení konstruktoru TResId umožňuje určit požadovaný element uložený v resource buď identifikátorem anebo identifikačním číslem, čili číselným jménem. Zadání řetězce s identifikátorem ikony vyvolalo první konstruktor. Ten se použil pro automatickou konverzi řetězce na objekt typu TResId. Samotná třída TFrameWindow je odvozená od třídy TWindow, základního okna bez podpory ovládání menu. TFrameWindow zahrnuje deklarace všech TWindow metod a dat, mezi něž patří i struktura Attr, typu TWindowAttr. Ta obsahuje řadu členů popisujících tvar okna stejně jako v programu pro Windows API (strana 126) - jeho atributy, umístění okna na obrazovce a identifikátor akcelerační tabulky, uložené v resource souboru aplikace (tj. modulu aplikace). TEventHandler
TModul
TWindow TWindowAttr Attr;
TApplication virtual int Run();
virtual void InitMainWindow();
TFrameWindow * SetMainWindow(TFrameWindow * );
TFrameWindow virtual bool AssignMenu(TResId ); bool SetIcon(TModule*, TResId ); Podpůrná třída
TResId
Obr. 6-1 Přehled použitých tříd a metod Struktura Attr obsahuje také člen TResId Menu; ale ten se může pouze číst. Menu představuje složitější prvek a jako takový se k oknu musí připojit metodou třídy TFrameWindow: virtual bool AssignMenu(TResId menuResId); Ta je deklarovaná jako virtuální, ale toho nevyužijeme, protože činnost metody nám vyhovuje !' a není potřeba změna jejího chování. Menu připojíme zadáním identifikátoru menu uloženého v modulu aplikace. Po jeho přiřazení se musí určit aktivní hlavní okno aplikace. K tomu se použije metoda třídy TApplication: TFrameWindow* SetMainWindow(TFrameWindow* window);
!'
Hodně metod v OWL má virtuální deklarace, i když se v praxi nikdy nenahrazují jinými definicemi. Slouží jako rezerva pro speciální situace.
138
Přeložíme-li uvedený program, uvidíme, že kromě titulku a ikony přibylo i menu, avšak jeho položky jsou neaktivní, díky správě zabudované v TFrameWindow, která detekovala, že povely se neobsluhují a nahlásila to uživateli. Avšak předtím, než se podíváme, jak se dají do objektů připojit metody pro obsluhu menu, musíme se zmínit o oknech klientech. Poznámka: InitMainWindow představuje virtuální metodu (viz. kap. 3.12 na str. 91), a proto ji lze pokládat za jakousi odbočku z kódu prováděného v metodě Run třídy TApplication. Její předefinování umožnilo pouze změnu části inicializací hlavního okna aplikace. Po skončení InitMainWindow budou dál pokračovat operace metody Run. Ty zavolají příslušné služby probírané ve Windows API, které hlavní okno řádně zaregistrují a vytvoří s využitím parametrů nastavených v InitMainWindow. Poté metoda Run zobrazí vzniklé okno a bude provádět smyčku zpráv až do přijetí zprávy WM_QUIT, značící konec aplikace.
6.1a
Okno jako klient okna
Grafické prvky Windows vycházejí svou podstatou z oken. Těmi jsou tlačítka, dialogy, list boxy a jiné prvky. Dokonce i běžné okno, používané programy, se skládá z několika jednodušších oken, jako rolovacích lišt, tlačítek na zavření, zvětšení a minimalizaci, i když ty spravuje výhradně OS. Okno představuje obdélník dovolující práci s obrazovkou. Lze nastavit jeho vzhled a propojit ho s dalšími systémovými prvky, jako s menu, s ikonou, s akcelerační tabulkou. Existuje pro něj grafická podpora - například relativní souřadnicový systém, automatické omezení kresby na hranicích (clipping) a možnost vytvořit DC (device context) a jiné prvky. Okno má callback funkci, které se mohou posílat zprávy, mezi nimi zprávy o pohybu myši, pokud se týkají okna. OS přepíná aktivitu okna, což má význam pro zprávy od klávesnice, a dále hlídá jemu přidělenou plochu, akumuluje oblast nutnou pro překreslení a posílá WM_PAINT, je-li viditelná. Jak řečeno, okno zprostředkovává řadu užitečných služeb OS, které lze snadno využívat. Nejjednodušší okno, tj. bez všech přídavných prvků, znamená neorámovaný obdélník, jakousi vymezenou plochu disponující vlastním systémem zpráv a úplnou grafickou podporou. Této skutečnosti se využívá ve třídě TFrameWindow a na jeho ploše se definuje okno klient. Přímé kreslení do okna vytvořeného TFrameWindow není výhodné z několika důvodů. Třída TFrameWindow obsluhuje řadu zpráv, kvůli podpoře menu, a uživatel by musel přepisovat hodně virtuálních funkcí, pokud by je chtěl využívat také, což by znevážilo veškerý zisk z objektového přístupu. Na druhou stranu, byla by škoda v zájmu univerzálnosti vzdát se podpory menu v hlavním oknu aplikace, protože tu využívá drtivá většina programů. Elegantní řešení nabízí zdvojení okna pomocí klienta, tím dostaneme jedno univerzální okno, s jehož plochou se pracuje, a druhé okno obsluhující menu. Klientem bývá nejjednodušší okno OWL a to TWindow, odpovídající klasickému oknu Windows. Od TWindow odvodíme okno MojeOkno, aby se mohl modifikovat jeho vzhled, a to učiníme klientem. Zásah si bude žádat následující úpravy (změny jsou vyznačené tučně): #include // vložení deklarací třídy TApplication #include // vložení deklarací třídy TFrameWindow class MojeOkno : public TWindow // okno klient { public: // konstruktoru přidělíme atribut přístupu public MojeOkno(TWindow* parent) // parent = nadřízené okno : TWindow(parent) // volání konstruktoru základní třídy { // operace konstruktoru MojeOkno /* Sem se mohou později vložit případné inicializace dat MojeOkno */ } };
139
class MojeAplikace : public TApplication // odvození nové třídy { public: MojeAplikace() : TApplication() {} // nutné volání konstruktoru základní třídy virtual void InitMainWindow() // nahrazení virtuální metody naším kódem { TFrameWindow * pwnd; // pointer na prvek třídy TFrameWindow pwnd = new TFrameWindow( 0, " OWL aplikace", // nadřízené okno + titulek okna new MojeOkno(0) // vytvoření klienta ); pwnd->SetIcon(this, "WINAPI_ICON"); pwnd->AssignMenu("WINAPI_MENU"); pwnd->Attr.AccelTable = "WINAPI_MENU"; pwnd->Attr.X=10; pwnd->Attr.Y=10; pwnd->Attr.H=400; pwnd->Attr.W=400; // umístění okna SetMainWindow(pwnd); // nastavíme hlavní okno aplikace } }; int OwlMain( int /*argc*/, char * /*argv*/ [] ) // Začátek programu { return MojeAplikace().Run(); // Metoda Run aplikovaná na dočasný objekt } Objekt MojeOkno se alokoval dynamicky, stejně jako TFrameWindow, a přitom samozřejmě nevzniklo okno, ale pouze se inicializovala data objektu. Vlastní vytvoření okna klienta provádějí odpovídající metody TFrameWindow, volané při jeho vytváření (v našem případě v metodě Run, protože TFrameWindow bude hlavním oknem aplikace a MojeOkno jeho klientem). Konstruktoru MojeOkno se nezadává nadřízené okno, protože v době jeho provádění se ještě nemůže znát; bude jím TFrameWindow, které vznikne až v následujícím kroku. V TFrameWindow se s tím počítá a odpovídající metody samy nastaví MojeOkno jako podřízené okno. Pokud se předchozí program přeloží, okno s klientem se vzhledem neliší od předchozího okna. Z hlediska struktury znamená však významný pokrok, protože v MojeOkno lze již vytvořit vše potřebné, a tak v další části budeme pracovat výhradně s ním. Do něho přidáme i nejdůležitější prvek aplikací - obsluhu událostí, čili event handlers.
6.1b
Event handlers - obsluha událostí
Má-li program pro Windows API reagovat na nějakou událost, je třeba vytvořit příslušnou obslužnou funkci a do callback funkce příslušného okna vložit kód detekující zprávu, obvykle nový case příkaz do switch příkazového bloku. V OWL se postupuje obdobně. Pro zjednodušení práce se využívají makra, která se expandují na potřebné úseky zdrojového kódu. Podpora pro obsluhu zpráv se vkládá na dvě místa - do deklarace třídy a vně ní. Do třídy se zařadí makrem DECLARE_RESPONSE_TABLE( identifikátor_tøidy ), jehož parametr tvoří identifikátor třídy okna, do níž ho vkládáme. Makro se expanduje na deklarace potřebných metod. Vně třídy se pak umístí dvě makra zajišťující definici tabulky, která popisuje přiřazení mezi zprávou a obslužnou funkcí, response function. Makra ohraničují začátek a konec tabulky: DEFINE_RESPONSE_TABLE1(identifikátor_třidy, identifikátor_základní_třidy) END_RESPONSE_TABLE; " Úvodní makro dostává dva parametry, identifikátor třídy a její bezprostřední základní třídy.
"
Identifikátor makra je zakončený číslicí 1, což naznačuje, že existují i obdobná makra mající čísla 2, 3, atd. Ta slouží pro případy vícenásobné dědičnosti (viz. konce kapitol 3.4 a 3.7). Např. makro pro dvojnásobné dědění: DEFINE_RESPONSE_TABLE2(tag_tøidy_okna, tag_základní_tøidy1, tag_základní_tøidy2) by se použilo, kdyby okno tag_tøidy_okna vzniklo dědění současně od dvou jiných tříd. To se dělá ojediněle. Vícenásobná dědičnost se mnohem častěji nahrazuje přidáním objektů jako členů třídy (viz. kap.3.9 na str. 84).
140
class MojeOkno : public TWindow // odvození nové třídy { public: MojeOkno(TWindow* parent) : TWindow(parent) { } protected: /* Sem se budou vkládat deklarace funkcí obsluhujících zprávy */ DECLARE_RESPONSE_TABLE(MojeOkno); // podpora pro uživatelovy zprávy v MojeOkno }; /* Tabulka pro třídu MojeOkno odvozenou od TWindow. Zde se definuje, které zprávy se obsluhují a jakými metodami*/ DEFINE_RESPONSE_TABLE1(MojeOkno, TWindow) /* Sem se budou vkládat definice zpráv a k nim odpovídajících metod */ END_RESPONSE_TABLE; Připojení obsluhy konkrétní zprávy se zadá makrem zprávy. Identifikátory maker zpráv mají jména vzniklá přidáním EV_ před označení zprávy. Například ke zprávě WM_PAINT patří makro EV_WM_PAINT, k WM_SHOWWINDOW zase makro EV_WM_SHOWWINDOW, a podobně. Výjimku tvoří pouze zprávy, u nichž se musí pro výběr obslužné metody analyzovat i její parametry, jako třeba zpráva WM_COMMAND. Její makro obsahuje dva parametry dovolující volbu obslužné metody podle identifikačního čísla prvku, který ji poslal: EV_COMMAND(id, identifikátor_metoda). Dále se musejí do deklarace třídy doplnit vhodné metody obsluhující událost, o níž zpráva nese informaci. Mohou se definovat jako externí nebo jako inline metody a obvykle se jim dává atribut přístupu protected, aby je směly používat pouze odvozené třídy. Všechny obslužné metody zpráv mají pevně stanovené argumenty. Zprávy, k nimž existuje makro bez parametrů, se navíc obsluhují metodami s předdefinovanými identifikátory. Příslušné informace o tvaru metod se vyhledají v nápovědě k překladači a nakopírují na odpovídající místa kódu. Například, pro zprávu WM_PAINT, se v nápovědě k OWL uvádějí tyto prvky: Makro Deklarace obslužné funkce EV_WM_PAINT void EvPaint() Přidání obsluhy této zprávy znamená tuto změnu: class MojeOkno : public TWindow // odvození nové třídy { public: MojeOkno(TWindow* parent) : TWindow(parent) { } protected: void EvPaint(); // deklarace obslužné metody DECLARE_RESPONSE_TABLE(MojeOkno); };
// podpora pro uživatelovy zprávy v MojeOkno
DEFINE_RESPONSE_TABLE1(MojeOkno, TWindow) EV_WM_PAINT, //do tabulky přidána detekce zprávy END_RESPONSE_TABLE; void MojeOkno :: EvPaint() { /*....*/ }
// Vlastní metoda EvPaint
Obslužnou metodu, event-handler, jsme přidali jako externí funkci, ale můžeme ji definovat i inline. Například, lze tak připojit reakci na zprávu WM_SIZE, kterou okno dostane při každé změně svojí velikosti. Ta má následující prvky: Makro EV_WM_SIZE
Deklarace obslužné funkce void EvSize(uint sizeType, TSize & size)
a její přidání do kódu vyvolá změny:
141
class MojeOkno : public TWindow // odvození nové třídy { public: MojeOkno(TWindow* parent) : TWindow(parent) { } protected: void EvPaint(); // zpráva WM_PAINT void EvSize(uint sizeType, TSize & size) // zpráva WM_SIZE { Invalidate(); } /* Metoda odpovídající InvalidateRectangle. Nastaví plochu celého okno jako potřebující překreslení. */ DECLARE_RESPONSE_TABLE(MojeOkno); // podpora uživatelových zpráv v MojeOkno }; DEFINE_RESPONSE_TABLE1(MojeOkno, TWindow) EV_WM_PAINT, EV_WM_SIZE, //do tabulky přidána detekce zprávy END_RESPONSE_TABLE; void MojeOkno :: EvPaint()
{ /*....*/ }
// Vlastní definice metody EvPaint
Při každé změna velikosti okna se teď volá metoda Invalidate. Ta je definovaná ve třídě TWindow a odpovídá volání InvalidateRectangle s parametry, určujícím celou plochu okna jako nutnou pro překreslení. Přidání této zprávy je v OWL nezbytné, chceme-li upravit obsah okna při změně jeho velikosti. Ve Windows API se překreslení zajišťovalo atributy při registraci třídy okna (viz. strana 126), jimiž se zadalo automatické poslání WM_PAINT při změně rozměrů okna. Registraci teď ale provádějí metody třídy okna bez nastavení těchto atributů, protože doplnění této vlastnosti, jak bylo ukázané, není obtížné, zatímco její rušení by bylo složitější. Zpráva WM_COMMAND používá následující definice makra a obslužné funkce: Makro Paramentry Deklarace obslužné funkce EV_COMMAND CMDID, UserName void UserName() Makro dovoluje volbu identifikátoru obslužné metody, ale její argumenty jsou pevně stanovené. Například zařazení metody CmKonec jako obslužné funkce pro zprávu WM_COMMAND, zaslanou položkou menu s identifikačním číslem CM_KONEC, by vypadalo takto: #include "winapi.rh" // soubor s deklaracemi identifikátorů položek menu class MojeOkno : public TWindow // odvození nové třídy { public: MojeOkno(TWindow* parent) : TWindow(parent) { } protected: void CmKonec() { Destroy(0); } // Zavření okna s exit kódem 0 void EvPaint(); void void EvSize(uint sizeType, TSize & size) { Invalidate(); } DECLARE_RESPONSE_TABLE(MojeOkno); }; DEFINE_RESPONSE_TABLE1(MojeOkno, TWindow) EV_COMMAND(CM_KONEC, CmKonec), EV_WM_PAINT, EV_WM_SIZE, END_RESPONSE_TABLE; void MojeOkno :: EvPaint() { /*....*/ }
// Vlastní definice metody EvPaint
Na začátku se vkládá soubor s definicemi identifikačních čísel prvků, popsaný v minulé kapitole. Vlastní obslužná metoda CmKonec volá metodu Destroy, patřící do TWindow, která uzavře okno - analogie k DestroyWindow ve Windows API. Destroy není potřeba předávat handle okna; jeho hodnota je totiž uložená v datech třídy. Metoda Destroy si ho odtud vezme a zavolá s ním DestroyWindow. (Metody tříd vždy provádějí veškeré operace pomocí adekvátních volání služeb Windows API, protože jiným povelům OS nerozumí.)
142
Uzavření okna klienta má za následek uzavření i TFrameWindow, což ukončí celou aplikaci. Poznámka: Zůstává otázka, proč se obsluha zpráv provádí makry a ještě tak složitě? Nestačilo by k metodě přidat nějakou vhodnou poznámku, která by specifikovala, že obsluhuje vybranou zprávu? Ano, šlo by to. Podobným způsobem to řešila objektová knihovna OWL verze 1. Její tvůrce Borland se tím dostal do sporu s normalizační komisí jazyka C++, protože ten stojí na myšlence, že činnost překladač vychází výhradně z pevně stanovené množiny klíčových slov a vše ostatní se vytváří pomocí nich. Tím se zaručuje přenositelnost jazyka na různé platformy. Povolují se sice některá rozšíření, ale způsob užitý v první OWL nebyl uznaný normalizační komisí za přípustný a firma Borland dostala na vybranou - buď pro svůj produkt přestane používat název jazyk C++, nebo změní obsluhu událostí. Kvůli tomu byla aplikovaná makra, která znamenají povolený prvek, protože se expandují na standardní C++ kód. Stejným postupem se realizuje obsluha zpráv i v jiných objektových knihovnách, jako třeba v MFC (a též ve VCL při tvorbě vlastních komponent). Odlišné jsou pouze identifikátory maker, viz. dále v kapitole 6.3.
6.1c
Výsledný zdrojový kód OWL aplikace
Pro přehlednost ukážeme napřed kód celé aplikace a teprve poté rozebereme jeho operace: #include // vložení deklarací třídy TApplication #include // vložení deklarací třídy TFrameWindow #include "winapi.rh" // soubor s deklaracemi identifikátorů položek menu class MojeOkno : public TWindow // odvození nové třídy { BOOL ctverec; // zobrazený čtverec BOOL elipsa; // zobrazená elipsa BYTE jasbarvy; // jas barvy 0..255 void Nuluj() { ctverec = elipsa = FALSE; BYTE jasbarvy = 128; } public: MojeOkno(TWindow* parent) : TWindow(parent) { Nuluj(); } void CheckMenu(int id, BOOL oznaceno ) // identifikátor položky a žádaný stav zatržení { ::CheckMenuItem( // Voláme funkci Windows API ( :: operátor určuje globální scope ) Parent ->GetMenu(), // Menu nadřízeného okna id, // Identifikátor položky menu MF_BYCOMMAND | (oznaceno ? MF_CHECKED : MF_UNCHECKED) // Žádaný stav ); } protected: void CmKonec() { Destroy(0); } // Zruš klienta, což zruší i hlavní okno a ukončí aplikaci void CmElipsa() { elipsa = !elipsa; CheckMenu(CM_ELIPSA, elipsa); Invalidate(); } void CmCtverec() { ctverec = !ctverec; CheckMenu(CM_CTVEREC, ctverec); Invalidate(); } void CmJasPlus() { if(jasbarvy<240) jasbarvy += 16; else jasbarvy=255; Invalidate(); } void CmJasMinus() { if(jasbarvy>=16) jasbarvy-=16; else jasbarvy = 0; Invalidate(); } void CmSmazat() { Nuluj(); CheckMenu(CM_ELIPSA, elipsa); CheckMenu(CM_CTVEREC, ctverec); Invalidate(); } void EvPaint(); void EvSize(uint sizeType, TSize& size) { Invalidate(); } DECLARE_RESPONSE_TABLE(MojeOkno); }; DEFINE_RESPONSE_TABLE1(MojeOkno, TWindow) 143
EV_COMMAND(CM_KONEC, CmKonec), EV_COMMAND(CM_ELIPSA, CmElipsa), EV_COMMAND(CM_CTVEREC, CmCtverec), EV_COMMAND(CM_JAS, CmJasPlus), EV_COMMAND(CM_JASMINUS, CmJasMinus), EV_COMMAND(CM_SMAZAT, CmSmazat), EV_WM_PAINT, EV_WM_SIZE, END_RESPONSE_TABLE; void MojeOkno::EvPaint() { char * text="Střed"; // zobrazený text TRect rect; // velikost plochy int xs, ys; // souřadnice středu plochy rect=GetClientRect(); // zjištění velikosti plochy xs=(rect.left+rect.right)/2; ys=(rect.top+rect.bottom)/2; // výpočet středu plochy TPaintDC dc(*this); // vezmeme si DC pro obsluhu zprávy WM_PAINT dc.SetBkMode(TRANSPARENT); // kreslíme transparentně if(elipsa) { TColor barva(0, 0, jasbarvy); TPen pen(PS_SOLID, 1, barva ); TBrush brush(barva); dc.SelectObject(pen); dc.SelectObject(brush); dc.Ellipse(rect); }
// ? kreslit elipsu // // // // // //
modrá barva, TColor ~ RGB pero pro plné čáry o síle 1 a modré barvě vytvoř modrý štětec, TColor ~ RGB nastav pero pro kreslení čar nastav štětec pro výplně nakresli elipsu se zadanou čárou a vyplní
if(ctverec) // ? kreslit čtverec { TColor barva(0, jasbarvy, 0); // zelená barva,, TColor ~ RGB TPen pen(PS_SOLID, 3, barva ); // pero pro plné čáry o síle 3 a zelené barvě TBrush brush(barva,HS_DIAGCROSS); // štětec pro křížové šrafování dc.SelectObject(pen); // nastav pero pro kreslení dc.SelectObject(brush); // nastav štětec pro výplně dc.Rectangle(xs-100, ys-100, xs+100, ys+100); // nakresli čtverec zadanou čárou a výplní } dc.SetTextColor(TColor(jasbarvy, 0, 0)); // psát červeně, barva dočasným objektem dc.SetTextAlign(TA_CENTER | TA_BASELINE); // poloha textu vůči bodu xs a ys dc.TextOut(xs, ys, text); // napíšeme text }
144
class MojeAplikace : public TApplication // třída aplikace { public: MojeAplikace() : TApplication() {} // nutné volání konstruktoru základní třídy virtual void InitMainWindow() // inicializace okna před jeho vytvořením { TFrameWindow * pwnd; // pointer na prvek třídy TFrameWindow pwnd = new TFrameWindow( 0, // není nadřízené okno (parent) " OWL aplikace", // titulek okna TFrameWindow new MojeOkno(0) // klient okna TFrameWindow ); pwnd->SetIcon(this, "WINAPI_ICON"); // ze souboru aplikace přidáme ikonu pwnd->Attr.AccelTable = "WINAPI_MENU"; // přidáme akcelerační tabulku pwnd->Attr.X=10; pwnd->Attr.Y=10; // poloha levého horního rohu okna pwnd->Attr.H=400; pwnd->Attr.W=400; // výška (height) a šířka (width) okna pwnd->AssignMenu("WINAPI_MENU"); // přidáme menu SetMainWindow(pwnd); // určíme hlavní okno aplikace } // konec InitMainWindow }; // konec TApplication int OwlMain( int /*argc*/, char * /*argv*/ [] ) // OwlMain s typy argumentů { return MojeAplikace().Run(); } // Metoda Run aplikovaná na dočasný objekt MojeAplikace Data nesoucí informaci o obraze, dříve globální, se stala privátními prvky třídy, což snižuje riziko chyby, protože s nimi pracují pouze metody třídy. Jejich inicializace se posunula do konstruktoru, kde se provádí metodou Nuluj, jejíž kód se využívá i v operaci mazání. Menu patří nadřízenému okno. TFrameWindow počítá s existencí klienta a veškeré zprávy od menu posílá podřízenému oknu. Použijeme už známou službu Windows API, funkci CheckMenuItem. Operátor :: globálního scope, který je před ní, zde není nutný, avšak bývá zvykem ho uvádět pro zdůraznění, že se volá služba OS, a ne metoda třídy. Pro práci s menu potřebujeme jeho handle. To zjistí GetMenu (metoda třídy TWindow a tím i TFrameWindow), avšak musí se volat jako prvek okna TFrameWindow, protože MojeOkno je pouhým klientem a žádné menu nemá. V programu se to provedlo přes pointer Parent, člena třídy MojeOkno, v němž je uložená informaci o nadřízeném oknu. Poznámka: Jinou možnost by představovala metoda TWindow::GetApplication, která by zjistila pointer na aplikaci typu TApplication *, z něho by se TApplication::GetMainWindow získal pointer na hlavní okno typu TFrameWindow *. Ten by pak dovolil jednak vzít si handle menu "GetApplication()->GetMainWindow()->GetMenu()" a jednak pomocí GetMenuDescr získat deskriptor menu, pointer typu TMenuDescr * dovolující měnit obsah položek menu. Ve třídě TMenuDescr je současně definované objektové CheckMenuItem. void CheckMenu(int id, BOOL oznaceno ) // identifikátor položky a žádaný stav zatržení { GetApplication()->GetMainWindow()->GetMenuDescr()->CheckMenuItem( id, // Identifikátor položky menu MF_BYCOMMAND | (oznaceno ? MF_CHECKED : MF_UNCHECKED) // žádaný stav ); } Nejelegantnější by však bylo na Parent použít TYPESAFE_DOWNCAST... ale raději už ne - na demonstraci toho, že k požadovanému cíli nemusí vést jediná cesta, to už stačilo. Vlastní kód metody CheckMenu, pro změnu zatržení položky menu, se podobá Windows API, až na úsporný zápis pomocí operátoru ? : místo původní if - else příkazu. Obsluha událostí - probírala se již v předchozí kapitole. Metody, které ji provádějí, byly definované jako inline a jejich kód se prakticky shoduje s programem pro Windows API. Liší se
145
jen tím, že se CheckMenu již nepředává handle okna; ten tvoří součást třídy. Dále se místo InvalidateRectangle celé plochy okna používá metoda Invalidate. EvPaint - kód se oproti Windows API výrazně zjednodušil. Změny ukazuje tabulka: Windows API
MojeOkno::EvPaint
RECT rect;
TRect rect; GetClientRect(hWnd, &rect);
PAINTSTRUCT ps; HDC hDC =
rect = GetClientRect(); // èlen objektu TPaintDC ---
BeginPaint ( hWnd, &ps ); SetBkMode(hDC, TRANSPARENT);
TPaintDC dc(*this); dc.SetBkMode(TRANSPARENT);
COLORREF barva =
RGB(0, 0, jasbarvy);
TColor barva(0, 0, jasbarvy);
HPEN hpen =
CreatePen(PS_SOLID, 1, barva );
TPen pen(PS_SOLID, 1, barva );
SelectObject(hDC, hpen); HBRUSH hbrush =
CreateSolidBrush(barva);
dc.SelectObject(pen); TBrush brush(barva);
Ellipse(hDC, rect.left, rect. top, rect.right, rect.bottom);
dc.Ellipse(rect);
TextOut (hDC, xs, ys, (LPSTR) text, lstrlen(text) );
dc. TextOut (xs, ys, text );
EndPaint ( hWnd, &ps );
// provádí destruktor dc
DeleteObject(...);
// provádìjí destruktory
Odpadla nutnost rušit každý objekt, protože tuto povinnost převzaly destruktory objektů. Jejich konstruktory a metody zase nabídly i možnost několika různých volání, viz. přetěžování funkcí probírané v objektech v kapitole 3.2 na straně 66. Kromě toho lze vynechávat i některé parametry, které mají výchozí (default) hodnoty. Téměř každému datovému prvku Windows odpovídá adekvátní objekt v OWL. Jak už víme z kapitol o objektech, objekt totiž představuje spojení dat s obslužnými metodami a pomocí toho se k obyčejnému handle dají přidat konstruktory, destruktory a metody. Místo zabrané v paměti se tím nemění. TColor zaujímá stejně velkou paměť jako RGB (tj. číslo long), avšak navíc nabízí i prostředky pro manipulaci s barvou. TBrush, TPen - analogie objektů z Windows API. Oproti nim nabízejí více možností vytvoření. TRect - pomocná matematická třída, přímá analogie RECT, avšak obsahující pomocné metody pro výpočet výšky, šířky, připočtení ofsetu a jiné. TColor - pomocná třída, odpovídá RGB, avšak obsahuje i předdefinované konstanty pro nejběžnější barvy. Ty jsou typu static, a proto se mohou používat bez objektu, např. TColor::Black, TColor::White, TColor:: LtGreen, (Light Green = světle zelená). Kromě toho zahrnuje devět různých konstruktorů a metody pro separaci barevných složek. TPaintDC - objekt je odvozený od TDC, základního objektu pro tvorbu DC, device context. Obsahuje minimum datových členů, prakticky jenom handle, dále strukturu PAINTSTRUCT a pomocné proměnné pro zapamatování výchozího stavu. Naproti tomu definuje přes dvě stě metod pro kreslení, analogií stejnojmenných služeb Windows. Mnoho z nich je přetíženo a lze je používat s různými parametry, viz. například již uvedenou Ellipse: bool Ellipse(int x1, int y1, int x2, int y2); bool Ellipse(const TPoint& p1, const TPoint& p2); bool Ellipse(const TPoint& point, const TSize& size); bool Ellipse(const TRect& rect); takže lze zvolit to volání, které se nám nelépe hodí. Kromě toho se pro mnoho parametrů uvádějí inicializační hodnoty, jako třeba: bool TextOut(int x, int y, const char far* string, int count = -1);
146
kde -1 znamená, že se píše celý řetězec. Tím se zjednodušuje zápis, protože ve většině případů se píše celý řetězec a lze vynechat parametr délky. TGdiBase
TGDIObject
TPallete Barevná paleta
Základní objekt pro GDI - primární definice pro jiné třídy
TIcon
TCursor
Ikony
Kurzory
TBrush Štětec pro výplně
TRegion
TFont
Definice plochy v okně MFC CRgn
Písmo
Device Context - kreslicí metody
TDC
TPen Pero pro kreslení čar
TBitmap
TDib
GDI závislé bitmapy
DIB bitmapy nezávislé na grafickém zařízení
TWindowDC
DC ke kreslení při WM_PAINT
TPaintDC
TDesktopDC
TScreenDC
TClientDC
DC ke kreslení po desktop
DC ke kreslení po celé obrazovce
DC ke kreslení po ploše okna
TDibDC
TPrintDC
TMemoryDC
DC ke kreslení do DIB bitmap
DC ke kreslení po celé obrazovce
DC ke kreslení po ploše okna
TCreateDC DC ke kreslení do paměti
Obr. 6-2 Pøehled DC objektù Pozn.: Prvky s názvy kurzívou mají ekvivalenty v MFC lišící se záměnou úvodního T za C.
6.1d
Náměty pro rozšíření aplikace
Naše jednoduchá aplikace složí jako ilustrativní příklad a nevyužívá plně nabízených možností. Ukažme si na závěr aspoň dva náměty, jak by se dala rozšířit. V textu jsme hovořili o podpoře obsluhy menu, avšak jedinou operaci s menu jsme provedli pomocí Windows API. Podpora totiž spočívá především v automatickém uvolňování (enable) a blokování (disable) příslušných položek. Předpokládejme, že chceme, aby položka Smazat byla aktivní pouze tehdy, když se zobrazuje čtverec nebo elipsa, a v ostatních případech, když není co mazat, byla zablokovaná, tj. napsaná šedě (grayed). K tomu stačí přidat nový event-handler, který se nazývá uvolňovač příkazů, command enabler, a vytvořit vhodnou metodu. Její název si můžeme zvolit, ale argument má pevně stanovený - jejím objekt ce, typu TCommandEnabler. Do tabulky odezev na zprávy vlo147
žíme makro mající podobný tvar jako pro WM_COMMAND, až na doplnění slova ENABLE, které bude na novou metodu odkazovat. TFrameWindow dovoluje zařadit uvolňování menu buď přímo do něj nebo do klienta a oba způsoby lze kombinovat. Vložíme-li obsluhu zprávy do MojeOkna, pak bude: /*... Deklarace objektu MojeOkno ...*/ void CmSmazat() { Nuluj(); CheckMenu(CM_ELIPSA, elipsa); CheckMenu(CM_CTVEREC, ctverec); Invalidate(); } void CmSmazatEn(TCommandEnabler& ce) // nová metoda pro ovládání menu { ce.Enable( elipsa || ctverec ); } // podmínka, kdy je položka menu aktivní /*... pokračování deklarací objektu MojeOkno ...*/ /*... Tabulka odezev na zprávy ...*/ EV_COMMAND(CM_SMAZAT, CmSmazat), EV_COMMAND_ENABLE(CM_SMAZAT, CmSmazatEn), // přidané makro /*... pokračování tabulky odezev na zprávy ...*/ V CmSmazatEn se provádí výpočet logické podmínky, nabývající hodnoty TRUE v situacích, kdy položka menu má být aktivní. Výsledek operace se předá metodou Enable objektu ce, typu TCommandEnabler. Vše ostatní zařídí vestavěná podpora menu. Ta během práce s menu bude posílat vhodné zprávy, takže se bude periodicky volat CmSmazatEn a zjišťovat aktuální stav položky, podle něhož se upraví zobrazení menu. Jako další rozšíření přidáme volbu fontu. Ve Windows API byla vynechaná pro značné množství parametrů. V OWL využijeme objektu TFont, v němž se definují inicializační hodnoty pro většinu parametrů, takže jich stačí vyplnit pouze část. /*....*/ dc.SetTextColor(TColor(jasbarvy, 0, 0)); // psát červeně, barva zadána dočasným objektem dc.SetTextAlign(TA_CENTER | TA_BASELINE); // poloha textu vůči bodu xs a ys TFont font("Times New Roman CE", 20); // font zadaný jménem a velikostí dc.SelectObject(font); // nastav font pro psaní dc.TextOut(xs, ys, text); // napiš text
6.2 Otevírání a zavírání oken v OWL
0.1a
Vytvoření okna
Vytvoření další okna, tedy jiného než hlavního, má v OWL stanovený postup: 1. Třeba definovat novou třídu, například NoveOkno, děděním od vhodného základu, třeba od TWindow příkazem: class NoveOkno : public TWindow 2. Z konstruktoru nové třídy se musí volat konstruktor základní třídy: NoveOkno:: NoveOkno(TWindow * parent, const char far* titulek): TWindow(parent, titulek); 3. V konstruktoru se dají inicializovat pouze data třídy, protože v době jeho provádění grafické okno ještě neexistuje. 4. Potřebujeme-li inicializovat grafické prvky, musíme předefinovat metodu: virtual void SetupWindow(); Ta se volá těsně po vytvoření okna, odpovídá WM_CREATE. Do ní vložit inicializaci grafických elementů okna.
148
5. Definovaný objekt vytvoříme, zpravidla dynamicky, například příkazem: NoveOkno * pNoveOkno = new NoveOkno(HlavniOkno, “Moje nové okno“); 6. Grafické okno vznikne voláním metody Create: pNoveOkno -> Create(); Windows inicializuji okno a přitom zavolají SetupWindow; Popsaný postup můžeme zobrazit graficky: new
Vytvořena data třídy Volání konstruktoru
Create()
Vytvořeno okno Volání SetupWindow()
6.2b
Rušení okna
Existují dva různé způsoby: I. Zavoláme metodu Destroy, např.: pNoveOkno -> Destroy(); ta vyvolá podobné akce jako: pNoveOkno -> SendMessage(WM_DESTROY); II. Zavoláme metodu CloseWindow, která provede akce jako: pNoveOkno -> SendMessage(WM_CLOSE); III. Je-li v menu okna èlen s identifikátorem CM_EXIT, pak volá CmExit událost. Ta provede CloseWindow, je-li okno hlavním oknem. Poznámky: • Teprve po zrušení okna je mo né zru it objekt pomocí delete. • Provést Create, Destroy a znovu Create bez zrušení dat je zcela legální. • Příklad na vytvoření okna lze získat na WEB stránce skripta.
6.3 MFC Knihovna OWL nebyla sice úplně první objektovou knihovnou, ale lze ji prohlásit za první ucelenou a více rozšířenou knihovnu pro programování Windows. Řadu let zaujímala přední postavení na trhu a inspirovala svou strukturou další firmy. Microsoft vytvořil svoji konkurenční knihovnu MFC, Microsoft Foundation Classes. MFC je na první pohled hodně podobná OWL, avšak při bližším zkoumání se projeví řada rozdílů, které lze ve stručnosti charakterizovat tím, že OWL představuje ryze objektovou knihovnu, zatímco MFC má charakter nadstavby nad Windows API. Sice používá objekty, ale mnohde porušuje pravidla objektového přístupu a rozhodně ji není možné považovat za vzorový příklad objektového programování. Místy připomíná spíš sbírku užitečných objektů než promyšlenou strukturu. Nicméně MFC se rozšířila více než objektově dokonalejší OWL a je na místě otázka proč. Lze to vysvětlit řadou důvodů. Zaprvé, MFC je mnohem jednodušší než OWL. MFC programátorovi stačí znát pouhé základy objektového programování, protože MFC neprovádí důslednou kon149
verzi Windows API do objektů a chybějí v ní náročnější abstrakce, které tvoří páteř OWL. V době uvedení na trh se proto MFC jevila jako mnohem srozumitelnější všem programátorům Windows, kteří tehdy psali převážně ve Windows API a přecházeli na objektový přístup. Paradoxně, stejně jako v případě Windows, i tentokrát zákazníky nejvíc lákalo nenáročné rozhraní a dali mu přednost před vnitřní dokonalostí. Kromě něho MFC disponovala výhodou, že patřila Microsoftu a nové prvky Windows se v ní objevovaly ihned, což sehrálo nemalou úlohu zejména v době masového přechodu na Win32. Výčet diferencí mezi oběma knihovnami by vydal na celou knihu. Uvedeme pouze hlavní odlišnosti MFC, které se týkají probraných částí OWL: • Řada MFC objektů používá podobná jména jako OWL, mnohdy se lišící jenom prvním písmenem; místo T se uvádí C, například TPen a CPen. • Prefix ON... v makrech zpráv byl nahrazen prefixem EV... , například ON_COMMAND místo EV_COMMAND, a k analogické výměně došlo i u názvů metod pro zprávy, z EvPaint se stalo OnPaint. • Změnily se názvy maker pro tabulku zpráv (viz. dále příklad), ale princip zůstal stejný. • Nepoužívá se žádný vstupní bod, ekvivalent OwlMain, a aplikace se spouští vytvořením statického objektu aplikace, např. MojeAplikace MojeApp; kde MojeApp vznikla děděním od CWinApp, tj. ekvivalentu TApplication. • Metodě TApplication::InitMainWindow odpovídá CWinApp::InitInstance. • Metoda TApplication::SetMainWindow byla nahrazená m_pMainWnd, public členem třídy CWinApp, do něhož se zapíše pointer na vytvořené okno. (Dost neobjektová operace!) • Okno CFrameWnd, ekvivalent TFrameWindow, nepoužívá okno klienta. • Okna se registrují s atributy zaslat zprávu WM_PAINT při změně rozměrů. • Místo virtuální funkce SetupWindow, používané pro inicializaci grafických prvků, se používá odezva na zprávu WM_CREATE nazvaná OnCreate. • Nejsou definované konstanty pro písmenné akcelerátory; místo VK_X se píše řetězec "X". • V MFC chybí ekvivalent třídy TResId pro abstraktní určení identifikátoru resource. Přehled neukazuje vážnější diference mezi oběma knihovnami - většinou jde o pouhé změny názvů. Vysvětlení podstatných rozdílů by vyžadovalo ponořit se při výkladu obou knihoven do mnohem větší hloubky. Teprve pak by vynikly výhody objektového přístupu OWL anebo naopak jednoduchost MFC (jak se komu co líbí), avšak to není účelem tohoto skripta. Také při přepisu našeho příkladu z OWL do MFC se projeví pouhé maličkosti. Chybějící ekvivalent TresId způsobí, že resource prvky musejí mít pro některé operace pevně stanovený typ identifikátoru, třeba třída CFrameWnd vyžaduje číselný identifikátor menu. V OWL díky TResId abstrakci lze vždy použít dvojí typ identifikátoru - jméno nebo číslo. Rovněž lze poukázat na zapsání pointru hlavního okna, vytvořeného v uživatelském programu, pomocí operátoru = do prvku m_pMainWnd s atributem přístupu public, což není zrovna doporučovaná objektová operace (zejména pro pointry). Aplikace pro OWL napsaná pomocí MFC by vypadala takto (rozdíly mezi oběma zdrojovými kódy jsou vyznačené tučně): /****************** WinAPI1.rh **********************/ #define WINAPI_MENU 100 // Menu pro CFrameWnd musí mít číselný identifikátor #define CM_KONEC 101 #define CM_ELIPSA 102 #define CM_CTVEREC 103 #define CM_SMAZAT 104 #define CM_JAS 105 #define CM_JASMINUS 106 150
/****************** WinAPI1.rc **********************/ #include "winapi1.rh" #include WINAPI_MENU MENU BEGIN POPUP "&Soubor" { MENUITEM "&Konec\tCtrl-X", CM_KONEC }
// Nutné definice pro překlad resource // WINAPI_MENU se nyní rovná 100 // Znak & zajistí podtržení písmene S // Značka \t zajistí odsazení textu Ctrl-X
POPUP "&Obrázek" { MENUITEM "&Čtvere&c\tF5", CM_CTVEREC MENUITEM "&Elipsa\tF6", CM_ELIPSA MENUITEM SEPARATOR // Příkaz vloží vodorovnou oddělující čáru MENUITEM "&Jas +\tF7", CM_JAS MENUITEM "J&as -\tF8", CM_JASMINUS MENUITEM SEPARATOR MENUITEM "&Smazat\tShift DEL", CM_SMAZAT } END WINAPI_MENU ACCELERATORS // WINAPI_MENU je název pro datový zdroj ACCELERATORS { VK_F5, CM_CTVEREC, VIRTKEY // F5, kód VK_F5 představuje označení překladače VK_F6, CM_ELIPSA, VIRTKEY // F6 VK_DELETE, CM_SMAZAT, VIRTKEY, SHIFT // Shift-Delete "X", CM_KONEC, VIRTKEY, CONTROL // Ctrl-X "Q", CM_KONEC, VIRTKEY, CONTROL // Ctrl-Q posílá stejné identifikační číslo jako Ctrl-X VK_F7, CM_JAS, VIRTKEY // F7 VK_F8, CM_JASMINUS, VIRTKEY // F8 } WINAPI_ICON ICON "winapi.ico" // WINAPI_ICON typu ICON se vloží z vnějšího souboru winapi.ico /****************** WinMFC.cpp **********************/ #include // MFC jádro a standardní prvky #include "winapi1.rh" class MojeOkno : public CFrameWnd // odvození nové třídy { BOOL ctverec; // zobrazený čtverec BOOL elipsa; // zobrazená elipsa BYTE jasbarvy; // jas barvy 0..255 void Nuluj() { ctverec = elipsa = FALSE; BYTE jasbarvy = 128; } public: MojeOkno() : CFrameWnd() {} // První konstruktor MojeOkno( // Druhý konstruktor UINT nResource, // identifikační číslo menu DWORD dwDefaultStyle = WS_OVERLAPPEDWINDOW, // styl CWnd *pParentWnd = NULL, // rodičovské okno CCreateContext *pContext = NULL // výchozí DC ) { LoadFrame(nResource, dwDefaultStyle, pParentWnd, pContext); Nuluj(); } 151
protected: void CheckMenu( int id, // identifikátor prvku menu BOOL oznaceno // stav zatržení ) { GetMenu()->CheckMenuItem( id, MF_BYCOMMAND | (oznaceno ? MF_CHECKED : MF_UNCHECKED) ); } void CmKonec() { DestroyWindow(); } void CmElipsa() { elipsa = !elipsa; CheckMenu(CM_ELIPSA, elipsa); Invalidate(); } void CmCtverec() { ctverec = !ctverec; CheckMenu(CM_CTVEREC, ctverec); Invalidate(); } void CmJasPlus() { if(jasbarvy<240) jasbarvy += 16; else jasbarvy=255; Invalidate(); } void CmJasMinus() { if(jasbarvy>=16) jasbarvy-=16; else jasbarvy = 0; Invalidate(); } void CmSmazat() { Nuluj(); CheckMenu(CM_ELIPSA, elipsa); CheckMenu(CM_CTVEREC, ctverec); Invalidate(); } void OnPaint(); DECLARE_MESSAGE_MAP() }; BEGIN_MESSAGE_MAP(MojeOkno, CFrameWnd) ON_COMMAND(CM_KONEC, CmKonec) ON_COMMAND(CM_ELIPSA, CmElipsa) ON_COMMAND(CM_CTVEREC, CmCtverec) ON_COMMAND(CM_JAS, CmJasPlus) ON_COMMAND(CM_JASMINUS, CmJasMinus) ON_COMMAND(CM_SMAZAT, CmSmazat) ON_WM_PAINT() END_MESSAGE_MAP() void MojeOkno::OnPaint() { char * text="Střed"; // zobrazený text CRect rect; // velikost plochy int xs, ys; // souřadnice středu plochy GetClientRect(&rect); // souřadnice středu plochy, CRect nemá operátor = xs=(rect.left+rect.right)/2; ys=(rect.top+rect.bottom)/2; // výpočet středu plochy CPaintDC dc(this); // vezmeme si DC pro obsluhu zprávy WM_PAINT dc.SetBkMode(TRANSPARENT); // kreslíme transparentně if(elipsa) // Má-li se kreslit elipsa { CBrush brush(RGB(0, 0, jasbarvy)); // vytvoř modrý štětec CPen pen(PS_SOLID, 1, RGB(0, 0, jasbarvy)); // pero pro plné čáry o síle 1 a modré barvě dc.SelectObject(brush); dc.SelectObject(pen); dc.Ellipse(rect); } if(ctverec) { COLORREF barva = RGB(0, jasbarvy, 0); // zelená barva CPen pen(PS_SOLID, 3, barva ); // pero pro plné čáry o síle 3 a zelené barvě CBrush brush(HS_DIAGCROSS,barva); // prohozené parametry oproti OWL dc.SelectObject(pen); dc.SelectObject(brush); dc.Rectangle(xs-100, ys-100, xs+100, ys+100); // nakresli čtverec zadanou čárou a výplní } dc.SetTextColor(RGB(jasbarvy, 0, 0)); dc.SetTextAlign(TA_CENTER | TA_BASELINE); dc.TextOut(xs, ys, (LPSTR)(text), lstrlen(text)); // oproti OWL nutno zadat délku řetězce } // konec OnPaint
152
class MojeAplikace : public CWinApp // odvození nové třídy { public: MojeAplikace() : CWinApp() {} // nutné volání konstruktoru základní třídy virtual BOOL InitInstance() // nahrazení virtuální metody naším kódem { // m_pMainWnd je prvek třídy CWinApp určující hlavní okno aplikace m_pMainWnd = new MojeOkno(WINAPI_MENU, WS_OVERLAPPEDWINDOW, NULL, NULL); m_pMainWnd->SetIcon(LoadIcon("WINAPI_ICON"),TRUE); // nahrání ikony m_pMainWnd->MoveWindow(10,10,400,400,FALSE); // určení polohy okna m_pMainWnd->ShowWindow(SW_SHOW); // ukaž okno m_pMainWnd->UpdateWindow(); // nastav celé okno pro překreslení return TRUE; } }; MojeAplikace MojeApp;
// Spuštění aplikace
Poznámka: O VCL knihovně, která nás bude zajímat nejvíce, se zmíníme až v další kapitole. Napřed se totiž musíme podívat na prvky typu control, z nichž vychází.
153
7 Prvky Control a komponenty Aplikace často vytvářejí podobné prvky, jako třeba tlačítka, seznamy a podobně, a vyplatí se zavést hotové elementy, jakési polotovary. Dnes se programy sestavují převážně z makro komponent, k nimž patří i prvky nazývané control. " Ty jsou obsažené ve Windows jako předdefinované třídy oken (Windows Common Controls). " Chovají se proto jako běžná okna, avšak liší se od nich přednastaveným vzhledem a chováním; obé lze částečně upravit - určit rozměry a umístění na ploše okna, titulek, formátování textu a podobně. Do základní skupiny prvků control, kompatibilních s Win16, patří: button - tlačítko, to má několik podskupin: push button - běžné tlačítko s textem, check box - text a políčko pro jeho označení, tj. výběr položky, radio button - skupina tlačítek s vlastnostmi voliče na rozhlasovém přijímači; právě jedno tlačítko může být vybrané a výběr jednoho zruší předchozí selekci, edit control - zobrazení, editace, vstup a výstup textu na jedné i několika řádkách, list box - seznam textových položek, který lze prohlížet. Má příbuzný prvek: combo box - box doplněný o ediční pole scroll bar - horizontální a vertikální lišta pro rolování obrazu, static - statické prvky, které slouží pouze pro výstup, static text - běžný text, lze zadat formátování doleva, do středu a doprava, group box - rámeček kolem prvků, black box - čtverec. Ediční pole (edit control), list box a scroll bar rozšiřují Win32 o nové možnosti chování a dále přidávají i další nové prvky. Stručný přehled bude provedený na straně 161. Prvky control lze umístit do okna jako podřízená okna s využitím hierarchické struktury Windows, v níž každé okno má nejvýše jedno nadřízené rodičovské okno (parent) a vlastní několik podřízených oken (children) - viz. Obr. 4.1 na straně 109. Manipulace s oknem vyžaduje znalost jeho handle, avšak ten se ví až po vzniku jeho grafické podoby, a proto se s oknem musí nakládat jako s dynamickým objektem a uchovávat si jeho handle v proměnné, což komplikuje kód. Kvůli tomu zavádějí Windows zjednodušenou manipulaci pro prvky control. Lze na ně odkazovat buď klasickým způsobem přes handle okna; nebo z okna, které prvek vlastní, pomocí fixního identifikačního čísla přiděleného při vytvoření prvku, identifier of control, takže adresace se může opírat o předem známé konstanty. Prvky typu control používají systém zpráv jako jiná okna, ale některé zprávy náležejí pouze jim. Jejich zprávy lze proto rozdělit na tři skupiny: • Zprávy Windows - sem patří zprávy používané i jinými okny (jejich označení začíná WM_ = Windows Message). Titulek prvku control, třeba text tlačítka, lze změnit zasláním stejné zprávy jakou se zadává titulek okna - WM_SETTEXT s parametry: wParam = 0; // není použité, ale musí být rovné 0 lParam = (LPARAM) (LPCSTR) pszText; // adresa řetězce s textem nového titulku • Zprávy prvků control - do této skupiny náležejí speciální zprávy, které ostatní okna nezpracovávají. Kód takových zpráv začíná zkratkou odvozenou z typu prvku. Například nový text lze vložit na určenou pozici do seznamu list boxu, pokud se mu pošle zpráva LB_INSERTSTRING s parametry:
"
Pro termín prvek control mě nenapadl žádný vhodný český ekvivalent. Otrocky by se to dalo přeložit jako řízený prvek anebo podřízený. " O třídách se mluvilo v kapitole o Windows API, kde se napřed pro okno registrovala třída definující jeho základní zobrazení. Od té se pak vytvářela vzhledově podobná okna.
154
wParam = index; lParam = (LPARAM) (LPCSTR) lpsz;
// index položky, před níž bude vložený řetězec // adresa vkládaného řetězce
• Notification zprávy - těmi posílají prvky control hlášení o změně svého stavu nadřízenému oknu, rodiči (parent). Win16 kompatibilní control prvky k tomu využívají zprávu WM_COMMAND, zatímco nové prvky Win32 control prvky posílají zprávu WM_NOTIFY. Název notification zpráv obsahuje zpravidla skupinu N_ . Například ve Win32 se při stisknutí tlačítka pošle práva WM_COMMAND s parametry: HIWORD(wParam); // notification code prvku, pro stisk tlačítka = BN_CLICKED LOWORD(wParam); // identifikátor prvku control, nebo 0 pro akcelerátor, či 1 pro menu (HWND) lParam; // handle okna patřícího prvku control stejně tak, edit text pošle přes WM_COMMAND notification EN_CHANGE při změně textu. Specializované okno Windows, zvané dialog, umí na své ploše vytvořit prvky control z údajů v souboru resource nebo z dat zadaných programem. Kromě toho lze prvky control přidat do kteréhokoliv běžného okna a kombinovat je v něm s jinými elementy, aniž dojde k omezení jejich funkčnosti. Dialog oproti běžným oknům skýtá jedinou výhodu - možnost popsat vlastnosti prvků control v resource. Každému oknu lze určit (pomocí atributů CreateWindow) tři stupně obsluhy vstupu a výstupu: • Nemodální dialog (Modeless Dialog Box) - svým chováním odpovídá běžnému okno. Po jeho vytvoření pokračuje program dál a nečeká na zavření dialogu. Lze s ním pracovat souběžně s jinými okny. • Modální dialog (Modal Dialog Box) - představuje nejčastější případ dialogu. Používá pro komunikaci s uživatelem v situacích, kdy žádaná informace je zcela nezbytná pro další běh programu. Požaduje vykonání na něm zobrazeného úkolu a teprve po ukončení dialogu smí aplikace pokračovat v činnosti. Modální dialog zablokuje rodičovské okno a vytvoří si vlastní smyčku frontových zpráv, která pracuje až do jeho zavření, takže aplikace nepřijímá žádné frontové zprávy, jako povely od klávesnice, a nemůže na ně reagovat. Práce s jinou aplikací je však možná. Speciálním případem modálního dialogu je task modal dialog, tj. úlohově modální, určený pro situace, kdy se nezná handle nadřízeného okna a zadá se místo něho 0. Během úlohově modálního dialogu se zablokují všechna okna patřící úloze, do níž náleží aplikace, která vytvořila dialog. • Systémově modální dialog (System-Modal Dialog Box) - je podobný modálnímu dialogu, avšak navíc zablokuje všechna okna všech aplikací, nejenom svoje rodičovské. Musí s ním zacházet opatrně, protože ovlivňuje celý OS. Doporučuje se nepoužívat ho, není-li to opravdu nezbytné. Modální dialogy skýtají riziko zablokování. Pokud se omylem stanou neaktivní vlivem hrubé programátorské chyby (jako předáním aktivity jinému oknu pomocí systémové služby SetFocus a mnoha dalšími, méně nápadnými, způsoby - třeba nevhodným voláním GetPixel), pak dojde k věčnému čekání, tzv. deadlock, viz. také kapitola 4.54.5c na straně 110. Okna aplikace jsou zablokovaná a čekají na ukončení dialogu, ale ten není aktivní, a proto nepřijímá zprávy od myši a klávesnice, a tak ho nelze zavřít. U modálního dialogu napraví situaci buď přepnutí na jinou aplikaci a zpět (dialog se opět aktivuje) anebo násilné ukončení aplikace. Pokud se však deadlock přihodil v systémově modálním dialogu, vyléčí ho zpravidla jen tlačítko Reset.
7.1 Dialogy a prvky control na úrovni Windows API Každý dialog má svůj identifikátor, podobně jako ostatní prvky umístěné v resource. U dialogu jej jím unikátní identifikátor v množině všech dialogů patřících do souboru resource aplikace na obrázku IDD_DIALOG. Pro něj lze zvolit jméno, tak je tomu v našem příkladu, nebo číslo větší než nula, podobně jako u už probraných menu, ikon a akcelerátorů. Vzhled dialogu se navrhuje interaktivně v některém grafickém editoru, například v Resource Workshop. 155
Obr. 7-1
Jednoduchý dialog
Rovně i každému control prvku se musí přidělit číslo unikátní v rámci tohoto dialogu a větší než 0, nebo rovné -1, což znamená bez identifikátoru. Dialog na obrázku má čtyři prvky s identifikačními čísly IDC_STEXT, IDC_EDIT, IDC_BUTTON a IDC_LIST, jejichž hodnoty se mohou rovnat třeba 10,11,12 a 13. Výsledkem bude textová podoba souboru resource, která by pro dialog na obrázku vypadala takto: /* #define IDD_DIALOG 1 Pozor, chceme-li na dialog odkazovat jménem, nesmí se jeho identifikátor nahradit číslem! */ #define IDC_STEXT 10 #define IDC_EDIT 11 #define IDC_BUTTON 12 #define IDC_LIST 13 IDD_DIALOG DIALOG 36, 52, 207, 111 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Vstup údaje" // titulek dialogu FONT 10, "MS Sans Serif" // použitý font a jeho velikost { DEFPUSHBUTTON "OK", IDC_BUTTON, 73, 92, 50, 14 RTEXT "Zobraz seznam číslo: ", IDC_STEXT, 40, 11, 72, 12 EDITTEXT IDC_EDIT, 115, 11, 51, 12 LISTBOX IDC_LIST, 9, 30, 193, 55, WS_BORDER | WS_VSCROLL | LBS_USETABSTOPS } Hlavičku popisu tvoří definice umístění dialogu na obrazovce a jeho vzhledu určeném stylem popsaným stejnými konstantami jako styl běžného okna. Dále se v ní uvádí titulek a font platný pro veškeré texty v dialogu. Definice čtyř prvků control se nacházejí uzavřené v příkazovém bloku { }, každá obsahuje - přidělený identifikátor; čtyři čísla definující umístění - x a y souřadnice levého horní rohu, šířka a výška; a další individuální parametry - titulek a atributy prvku, uvedené u list box. • DEFPUSHBUTTON - tlačítko nastavené jako default prvek, čili po zobrazení dialogu dostane focus a stisk klávesy enter ho aktivuje. Tlačítko bude mít titulek "OK"; • RTEXT - statický text zarovnávaný doprava uvnitř vymezeného pole; • EDITTEXT - pole pro vstup textu; • LISTBOX - výpis nalezeného seznamu - bude mít kolem sebe rámeček, vertikální scroll bar a dovolí používat text s tabulátory. Samotné vytvoření dialogu na úrovni Windows API vyžaduje několik operací, ve Win32 velmi jednoduchých, avšak závisejících na tom, jedná-li se o nemodální či modální dialog. Nemo156
dální dialogy se z resource vytvoří pomocí CreateDialog a ruší DestroyDialog, zatímco modální dialogy se spouštějí voláním DialogBox a končí EndDialog. Náš dialog je modální, nejčastější případ, a proto si ukažme kód pro něj. Callback funkci dialogu nazveme třeba DialogProc. Ta je ekvivalentem callback funkce okna a její kód pro prostředí Win32 by vypadal takto: BOOL CALLBACK DialogProc( HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam ) { WORD wNotifyCode; WORD wID; switch (uMsg) { case WM_INITDIALOG: return (TRUE); case WM_COMMAND:
// // // //
handle dialogového okna identifikační číslo zprávy první parametr zprávy (má 32 bitů ve Win32!) druhý parametr zprávy - 32 bitů
// notification code - hlášení od prvku control // identifikační číslo přidělené prvku // nutná obsluha zprávy - inicializace dialogu // dialog byl inicializovaný a smí se zobrazit // notification zprávy od Win16 kompatibilních prvků control
wNotifyCode = HIWORD(wParam); // notification kód prvku ve Win32 (ve Win16 jinak!) wID = LOWORD(wParam); // získání identifikačního čísla prvku switch (wID) { case IDC_BUTTON: // zpráva od tlačítka if(wNotifyCode==BN_CLICKED) // tlačítko stisknuto EndDialog(hwndDlg, IDOK); /* služba OS - konec dialogu, jako jeho výsledek se vrací systémová konstanta IDOK (= 1) */ break; case IDC_EDIT: // notification zprávy od edičního pole break; case IDC_LIST: // notification zprávy od list boxu break; } // pozn. statický text neposílá zprávy, a proto zde chybí return TRUE; } return (FALSE);
// zpráva WM_COMMAND obsloužena // ostatní zprávy nejsou obsloužené, vrací se OS
} Poznámka: Výše uvedený kód by se pro nemodální dialog lišil pouze v operaci pro uzavřením dialogu, EndDialog by nahradilo DestroyDialog. Modální dialog se zobrazí funkcí DialogBox, která skončí teprve po uzavření dialogu, tj. po zavolání EndDialog. Příkaz DialogBox ve Win32 by vypadal takto: int iret; // Návratová hodnota = -1 při chybě, jinak argument EndDialog iret = DialogBox( // Vytvoř dialog a po jeho skončení vrať argument EndDialog hInstance, // Instance aplikace, argument WinMain "IDD_DIALOG", // Jméno dialogu hWnd, // handle okna vlastníka dialogu, = 0, není-li vlastník "! DialogProc // identifikátor callback funkce dialogu ); "!
Prostředí Win16 nedovoluje použít jako argument identifikátor DialogProc, tj. pointer na funkci, ale vyžaduje jeho konverzi pomocí služby MakeProcInstance. Ta je ve Win32 zbytečná.
157
Návratová hodnota iret se rovná -1, pokud se dialog nevytvořil, nebo odpovídá argumentu EndDialog. Zpravidla se pro ni volí identifikátor tlačítka, které dialog ukončilo, nebo vhodná systémová konstanta. V našem případě posíláme konstantu IDOK (= 1). Průběh zpráv ve vytvořeném dialogu ukazuje následující obrázek. BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
D
WM_SETTEXT, WM_GETTEXT WM_GETTEXTLENGTH
Podřízené okno statický text IDC_STEXT
i a
WM_COMMAND(EN_... + IDC_...) hlášení změn „ Notification “ zprávy
Podřízené okno edice textu IDC_EDIT
l o g -
EM_... + WM_... práce s prvkem pomocí zpráv
Podřízené okno tlačítko IDC_BUTTON
BN_... / BM_... DM_... WM_...
o k
K l á v e s n i c e
LBN_... / LB_... WM_...
a
Podřízené okno seznam IDC_LIST
n o
hWDialog1
Windows
iret = DialogBox( hInstance, "IDD_DIALOG", hWnd, DialogProc );
m y š
IDD_DIALOG HW11
IDC_STEXT
HW12
IDC_EDIT
HW13
IDC_BUTTON
HW14
IDC_LIST
Resource data: IDD_DIALOG Obr. 7-2 Modální dialog, jeho vytvoøení a zprávy Při vytváření dialogu si Windows uloží identifikační čísla přidělená prvkům control do interní tabulky, takže s prvky control lze pracovat dvojím způsobem, pomocí handle jejich okna nebo jejich identifikátorů. Mezi rodičovským oknem, v našem případě oknem dialogu, a prvky control proudí zprávy. Statický text žádné zprávy neposílá. Lze ho jen měnit, číst nebo zjistit jeho délku pomocí zpráv uvedených na obrázku. Editační pole přijímá zprávy a samo vysílá notification kódy při každé změně svého obsahu. Podobně se chová i tlačítko a list box. Zprávy prvkům control pošle SendMessage, přes handle jejich okna, anebo se znalostí handle jejich rodičovského okna a užitím identifikátorů pomocí funkcí na Obr. 7-3.
158
SetDlgItemText(hWnd, idControl, lpsz)
GetDlgItemText(hWnd, idControl, lpsz, cbMax)
SetDlgItemInt(hWnd, idControl, uValue, fSigned)
GetDlgItemInt(hWnd, idControl, lpfTrans, fSigned)
WM SETTEXT
WM GETTEXT
SendDlgItemMessage(hWnd, idControl, uMsg, wParam, lParam)
BM_SETCHECK HWDialog
CheckDlgButton(...) CheckRadioButton(...) BM GETCHECK
IDD DIALOG HW11
IDC STEXT
HW12
IDC EDIT
HW13
IDC BUTTON
HW14
IDC LIST
IsDlgButtonChecked(...)
hWndC=GetDlgItem(hWnd, idControl)
idControl=GetDlgCtrlID(hWnd)
hWndC = GetNextDlgTabItem(hWnd, hWndC, fPrevious) hWndC = GetNextDlgGroupItem(hWnd, hWndC, fPrevious)
Obr. 7-3 Práce s prvky control Základní funkci tvoří SendDlgItemMessage, která má pět argumentů. První specifikuje handle okna, vlastníka příslušného prvku control, jemuž se zpráva posílá, druhý udává identifikační číslo prvku control, adresáta zprávy, a poslední tři popisují zaslanou zprávu. Pro často používané zprávy existují zkratky. Zápis textu, zpráva WM_SETTEXT, se dá provést i zjednodušeně SetDlgItemText. Převedení čísla typu int na text a jeho následné poslání dokáže SetDlgItemInt, v níž fSigned určuje konverzi se znaménkem nebo bez něho. K oběma funkcím existují opačné. Text přečte GetDlgItemText s parametry ukazatel na text lpsz a maximální délka textu cbMax; čísla získá GetDlgItemInt, v níž argument lpfTranslate udává adresu proměnné typu BOOL, do níž bude uložena 0 při chybě, jinak nenulová hodnota. Označení u check box nebo radio button, nastavované zprávou BM_SETCHECK, lze provést i CheckDlgButton a CheckRadioButton. Naopak stav prvků zjistí IsDlgButtonChecked. Další služby OS umožňují převod mezi identifikačním číslem prvku control a handle jeho okna. Funkce GetDlgItem zjistí handle okna z identifikačního čísla a naopak GetDlgCtrlID najde k handle přidělené identifikační číslo. Převodní tabulku lze i prohledávat podle pořadí vzniku v souboru resource a dvou nastavených atributů prvků control - WS_TABSTOP a WS_GROUP. 159
Atribut WS_TABSTOP, zadaný u prvku control, dovoluje jeho selekci pomocí klávesy tabulátoru. Mají ho jako výchozí (default) všechny prvky control, reagující na vstup z klávesnice, tzn. není-li uvedeno jeho zrušení, pak je nastavený. Platí to třeba pro ediční pole nebo tlačítko (má-li focus, přijímá klávesu enter). Kdyby se u nich atribut zrušil a naopak přidal do list box, který ho jako výchozí nemá, protože pouze zobrazuje, změnil by se resource takto (nové prvky vyznačené tučně): /* Hlavička dialogu vynechaná - v ní se nic nemění */ { DEFPUSHBUTTON "OK", IDC_BUTTON, 73, 92, 50, 14, NOT WS_TABSTOP // zrušeni atributu RTEXT "Zobraz seznam číslo: ", IDC_STEXT1, 40, 11, 72, 12 EDITTEXT IDC_EDIT, 115, 11, 51, 12, NOT WS_TABSTOP // zrušeni atributu LISTBOX IDC_LIST, 9, 30, 193, 55, WS_BORDER | WS_VSCROLL | LBS_USETABSTOPS | WS_TABSTOP // přidání atributu } Další atribut WS_GROUP označuje první prvek skupiny. Do té pak patří všechny následující prvky, v pořadí jejich uvedení v resource, až k dalšímu prvku s WS_GROUP; ten bude prvním prvkem nové skupiny. Atribut se obvykle kombinuje s WS_TABSTOP, aby se tabulátorem dalo přeskočit na začátek skupiny. Mezi členy skupiny se dá přesouvat pomocí kurzorových šipek.
Obr. 7-4 Dialog se dvěma skupinami Skupiny mají největší význam pro radio button. V dialogu pro výběr dne a hodiny byly prvky radio button rozdělené na dvě skupiny pomocí atributů WS_GROUP, uvedených prvků s titulky Pondělí a 9:00. Tím se dosáhlo toho, že se prvky chovají jako dvě nezávislé trojice - z každé lze zvolit jeden prvek. Bez atributů WS_GROUP by tvořily jednu šestice. Obě skupiny ohraničuje group box, statický prvek s rámečkem a titulkem. Ten neovlivňuje funkci dialogu a slouží výhradně ke grafickému zvýraznění. První prvky skupin mají nastavené i atributy WS_TABSTOP, takže tabulátorem bude možné skákat na začátky skupin. Uvnitř nich se lze pohybovat kurzorovými šipkami, díky WS_GROUP. Tabulátor bude vybírat rovněž i tlačítko OK, které má atribut WS_TABSTOP nastavený jako výchozí. Soubor resource, lze opět navrhnout v interaktivním prostředí, třeba ve zmíněném Resource Workshop. V něm se zobrazují veškeré atributy a lze tam současně i otestovat chování dialogu. Výsledek grafického návrhu se uloží jako textový soubor: #define IDC_RADIOBUTTON1 101 // definice identifikátorů #define IDC_RADIOBUTTON2 102 #define IDC_RADIOBUTTON3 103 #define IDC_RADIOBUTTONA 104 #define IDC_RADIOBUTTONB 105 #define IDC_RADIOBUTTONC 106
160
DIALOG_DENHODINA DIALOG 8, 18, 110, 95 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Vyber den a hodinu" FONT 11, "MS Sans Serif" { CONTROL "Pondělí", IDC_RADIOBUTTON1, "BUTTON", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP, 11, 15, 34, 15 CONTROL "Středa", IDC_RADIOBUTTON2, "BUTTON", BS_AUTORADIOBUTTON, 11, 33, 34, 15 CONTROL "Pátek", IDC_RADIOBUTTON3, "BUTTON", BS_AUTORADIOBUTTON, 11, 50, 34, 15 CONTROL "9:00", IDC_RADIOBUTTONA, "BUTTON", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP, 64, 15, 34, 15 CONTROL "12:00", IDC_RADIOBUTTONB, "BUTTON", BS_AUTORADIOBUTTON, 64, 33, 34, 15 CONTROL "15:00", IDC_RADIOBUTTONC, "BUTTON", BS_AUTORADIOBUTTON, 64, 50, 34, 15 DEFPUSHBUTTON "OK", IDOK, 30, 76, 50, 14, BS_DEFPUSHBUTTON | WS_GROUP GROUPBOX "Den", -1, 8, 4, 40, 62, BS_GROUPBOX GROUPBOX "Hodina", -1, 61, 4, 40, 62, BS_GROUPBOX } První prvek každé skupiny má atribut WS_GROUP a současně i WS_TABSTOP. Poslední třetí skupina začíná tlačítkem OK, aby se zabránilo možnosti přejít na něj kurzorovou šipkou ze skupiny radio button. Tlačítko OK má atribut WS_TABSTOP jako výchozí a jeho identifikační číslo tvoří systémová konstanta IDOK, většinou přidělovaná tlačítkům OK. Přehled systémových konstant bude provedený později v této kapitole při popisu MessageBox. Oba group box nemají identifikační čísla, což značí použití -1, protože neposílají žádné zprávy a nepředpokládá se změna jejich titulků. Uvedený dialog můžeme prohledávat. Funkce GetNextDlgGroupItem ke známému handle okna hWndC prvku skupiny určí handle okna následujícího prvku skupiny, respektivě předchozího, přičemž prohledávání se provádí v pořadí, v jakém jsou prvky uvedené v souboru resource. Podobně pracuje i GetNextDlgTabItem, avšak prohledává prvky, které mají nastavený atribut WS_TABSTOP. Pokud žádný takový prvek neexistuje, vrací handle výchozího prvku hWndC. Dále lze pro prohledávání využít i GetFocus, funkci vracející handle okna, které má focus. Je-li zavolána při spuštění modální dialogu, vrátí handle právě aktivního prvku. Poznámka: Uspořádání control prvků do skupin dovoluje přechody pomocí kurzorových šipek, avšak nikoliv ve všech případech. Některé prvky používají šipky interně, jako třeba ediční pole, list box, combo box. Budou-li uvedené ve skupině, ovlivní to funkce prohledávající dialog, ale přechody šipkami nebudou možné, pouze tabulátory. Samozřejmě je na místě otázka - jak zařídit přechody kurzorovými šipkami i mezi edičními poli a dosáhnout toho, aby se chovaly jako buňky tabulky? Obtížně, musí se buď filtrovat odpovídajících zprávy od klávesnice ve smyčce frontových zpráv nebo si napsat vlastní ediční pole, respektivě ho odvodit vhodným děděním z objektové knihovny. Některé věci se ve Windows holt provádějí snadno, jiné zas hůř. Zdánlivé maličkosti mohou přerůst v týdny práce, a proto je lepší sestavovat programy se známých prvků.
7.2 Zprávy prvků control Všechna programovací prostředí, včetně vizuálních, se stýkají nejméně v jednom bodě - ve zprávách. Mění se jen terminologie, kterými se popisují. Metoda reagující na zprávu se zpravidla označuje jako event handler, nebo zkráceně jako event, bez rozdělování na běžnou a notification zprávu - dekódování provádějí objekty. Atributy prvků control se určují jako vlastnosti objektů, jimiž se nastavuje i vzhled oken, modální či nemodální režim jeho spuštění. Omezený rozsah skripta nedovoluje provést detailní přehled všech prvků control a jejich zpráv, a proto bude uvedený alespoň stručný přehled prefixů jejich zpráv. Ty se totiž vyskytují jako prvky ve všech objektech, které s nimi pracují. Pouze se někdy mění jejich názvy. Nicméně 161
znalost originální zprávy, zejména u notification zpráv, dovoluje v nápovědě k Windows API vyhledat údaje o tom, za jaké situace se zpráva posílá. Tabulka 1 - Přehled prefixů zpráv prvků control
Prvek Control
Zprávy
Notification
Použití
Push Button
BM_, DM_
BN_
Tlačítko.
Check Box Radio Button Edit-control List Box
BM_ BM_ EM_ LB_
BN_ BN_ EN_ LBN_
Volba několika prvků. Výběr jednoho prvku z několika. Edice textu. Seznam položek.
Combo box
CB_
CBN_
List box rozšířený edit-control. Rolování okna.
Scroll bar Static text
""
SBM_ "# ---
-----
Animation
ACM_
ACN
Přehrání AVI souborů.
Drag List Boxes
LB_
DL_, LBN
Header Hot-Key
HDM_ HKM_
HDN_ ---
Image Lists
---
---
List View
LVM_
LVN_
Progress Bars
PBM_
---
List Box rozšířený o možnost přetažení položek myší. Výběr jednoho sloupce z několika. Okno dovolující uživateli zadat klávesy zrychlené volby, tzn. neprovádějí se v něm akcelerátory. Zobrazení několika bitmap nebo ikon totožné velikosti. Seznam prvků, každý složený z ikony a názvu. Lišta udávající graficky podíl z délky nějaké operace. Několik překrývajících se dialogů, mezi nimiž lze přepínat. Edice formátovaného textu. Vodorovné okénko, do něhož lze vypisovat různé informace. Tlačítka ve tvarů lístků kartotéky, umožňující výběr z několika položek, často využívaná s Property Sheets. Výběrová lišta s tlačítky a nabídkami. Malé okno pro zobrazení řádku textu, často používané v toolbar. Stupnice a ukazatel, jehož polohu lze změnit myší. Zobrazení hierarchické struktury Dvě malá tlačítka se šipkami pro přičtení a odečtení. Používá obvykle vpravo u editcontrol.control.
Statický text.
+ W i n 3 2
Property Sheets PSM_
PSN_
EM_ Rich Edit Status Windows SB_
EN_ ---
Tab
TCM_
TCN_
Toolbars Tooltip
TB_ TTM_
TBN_ TTN_
Trackbars
TBM_
TB_
Tree View Up-down (spin)
TVM_
TVN_
UDM_
UDN_
""
Scroll bar control posílá zprávy WM_HSCROLL a WM_VSCROLL, tedy stejné jako scroll bar okna. Statický text se ovládá zprávami WM_GETTEXT, WM_GETTEXTLENGTH a WM_SETTEXT, což jsou zprávy užívané řadou prvků nepatřící pouze statickému textu. "#
162
7.3 Předdefinované dialogy
7.3a
Výstup textu
Windows zahrnují, kromě prvků control, i několik předdefinovaných dialogů (Common Dialog Boxes). MessageBox umí zobrazit jednoduchou zprávu. Má čtyřmi argumenty: HWND hwndParent // handle okna vlastníka dialogu. Není-li, pak =NULL LPCTSTR lpszText, // text, který smí mít i několik řádek LPCTSTR lpszTitle, // titulek okna dialogu UINT fuStyle // styl dialogu popsaný jako bitové OR určených atributů Jejich vliv na dialog ukazuje obrázek: MB_SYSTEMMODAL, MB_APPLMODAL (default ) ≈ MB_TASKMODAL
IDABORT IDCANCEL IDIGNORE IDNO IDOK IDRETRY IDYES
int MessageBox(HWindow hwndParent, char * text, char * titulek, uint typ)
MB_ICON ...HAND, ...STOP Jste si opravdu jisti
...ASTERISK, ...INFORMATION ...EXCLAMATION
?
...QUESTION
Chyba 98: V počítači objeven virus ! Chcete ho aktivovat ?
...STOP ANO
Ne
Storno
MB_OK MB_OKCANCEL MB_YESNO MB_YESNOCANCEL MB_RETRYCANCEL MB_DEFBUTTON1, MB_DEFBUTTON2 MB_DEFBUTTON3 Obr. 7-5 MessageBox Na obrázku jsou tučné zdůrazněné styly použité při volání: retval = ::MessageBox( NULL, " Chyba 98:\n V počítači objeven virus !\n Chcete ho aktivovat ?", " Jste si opravdu jisti ", MB_YESNOCANCEL | MB_ICONQUESTION | MB_DEFBUTTON2 | MB_TASKMODAL ); První argument NULL určil, že dialog nemá žádného vlastníka, a druhý zadal text zprávy - ten může mít neomezenou délku, prakticky limitovanou jen rozlišením obrazovky. Výjimku zde tvoří výhradně systémově modální dialog, dovolující zobrazení pouze tří řádek textu. Další
163
argument definoval text titulku okna, u něhož se povoluje jediný řádek. Poslední argument specifikoval vzhled a chování zobrazeného dialogu pomocí atributů: MB_YESNOCANCEL - tři tlačítka s odpovídajícími titulky podle jazykové verze OS MB_ICONQUESTION - vložení ikony s otazníkem MB_DEFBUTTON2 - druhé tlačítko dostane focus po spuštění. Není specifikováno, bude zvýrazněné první tlačítko. MB_TASKMODAL - stejná funkce jako MB_APPLMODAL, tj. modální dialog, ale určená pro případy, kdy se místo handle okna vlastníka použije NULL, viz. popis task modal dialogu na straně 155. Funkce vrací jako hodnotu systémovou konstantu, která odpovídá stisknutému tlačítku, v případě tlačítka "NE" to bude IDNO. Konstanty pro MessageBox se často používají i v ostatních dialozích pro tlačítka s odpovídajícími významy. MessageBox se obvykle vyskytuje i jako metoda v objektových knihovnách, a proto byl, v uvedeném příkladu, volaný s operátorem :: globálního scope, aby se zdůraznilo použití služby OS. Objektové metody MessageBox mají totožné argumenty, avšak s rozdílem vynecháním handle okna vlastníka, to se bere z třídy, odkud se metoda volá. MessageBox definovaný v OWL jako metoda třídy TWindow by se volal takto, samozřejmě za předpokladu odpovídajícího scope: retval = MessageBox( " Chyba 98:\n V počítači objeven virus !\n Chcete ho aktivovat ?", " Jste si opravdu jisti ", MB_YESNOCANCEL | MB_ICONQUESTION | MB_DEFBUTTON2 ); Není zde použitý atribut MB_TASKMODAL, protože vlastníkem bude příslušný objekt okna, z něhož se metoda zavolá. Dialog bude modální (MB_APPLMODAL = výchozí hodnota). Stejný dialog zahrnuje i knihovna VCL, ale jako prvek třídy TApplication. Win32 mají i MessageBoxEx; ten je oproti MessageBox rozšířený o argument udávající systémovou konstantu požadované jazykové verze zobrazení tlačítek. Význam ostatních parametrů se nemění: int MessageBoxEx( HWND hwndParent, LPCTSTR lpszText, LPCTSTR lpszTitle, UINT fuStyle, WORD wLanguageID // identifikátor jazykové verze ); Pro zobrazení zpráv jsou užitečné i tři další funkce, které sice nepatří do předdefinovaných dialogů, ale stojí za to vědět o tom, že existují: BOOL FlashWindow( // Blikání titulku okna pro upoutání pozornosti uživatele. hWindow, // Handle okna, které má změnit aktivitu. fInvert // FALSE - změň tam a zpět, TRUE -změn na opačný stav ); BOOL Beep( // Výstup na reproduktor DWORD dwFreq, // Frekvence zvuku v HZ z intervalu <0x25, 0x7FFF> = <37, 32767> DWORD dwDuration /* Délka zvuku v milisekundách. Je-li > 0, pak synchronní výstup, tj. návrat až po skončení zvuku. Je-li = -1, potom trvalý asynchronní zvuk, který se vypne se až dalším voláním Beep. */ );
164
void MessageBeep(uAlert);
/* krátké asynchronní pípnutí zvukem určeným ve WIN.INI, tj. aplikace nečeká na jeho skončení. */ -1 - Výstup na reproduktor /* uAlert = MB_ICONASTERISK - SystemAsterisk - hvězdička, oznámení MB_ICONEXCLAMATION - SystemExclamation - varování MB_ICONHAND - SystemHand - chyba - SystemQuestion - otázka MB_ICONQUESTION - SystemDefault - potvrzení */ MB_O K Poznámka: Předdefinovaných zvuky Windows skýtají nevýhodu prioritního výstupu na zvukovou kartu. Pokud byla nainstalovaná, nejsou slyšet bez připojených reproduktorů.
7.3b
Další předdefinované dialogy
Windows obsahují další předdefinované dialogy. avšak jejich použití na úrovni API vyžaduje složité vyplnění struktur, stejně jako při registraci okna, a lépe se volají před objektové knihovny. Pou ití
Windows API
Ekvivalent v OWL
C++Builder VCL
Ekvivalent v MFC
Výbìr barvy Výber fontu Soubor pro otevøení Soubor pro ulo ení Nastavení tiskárny
ChooseColor ChooseFont GetOpenFileName GetSaveFileName PrintDlg
TChooseColorDialog TChooseFontDialog TFileOpenDialog TFileSaveDialog TPrintDialog
CColorDialog CFontDialog CFileDialog
Hledání textu Nahrazení textu
FindText ReplaceText
TFindDialog TReplaceDialog
TColorDialog TFontDialog TOpenDialog TSaveDialog TPrintDialog TPrintSetupDialog TFindDialog TReplaceDialog
CPrintDialog CFindReplaceDialog
Obr. 7-6 Dialog - otevøení souboru Předdefinované dialogy objektových knihoven lze aplikovat snadno, stačí postupovat podle návodu. Vyžadují zapsat nezbytné údaje do datové struktury, respektivě do třídy, určené pro přenos parametrů a tu předat jako argument žádanému dialogu. Většina položek je předdefinovaná, takže se píší jen odlišnosti. Jako příklad si ukážeme předdefinovaný Windows dialog pro zadání jména souboru pro otevření. Využijeme OWL. Dialog na obrázku Obr. 7-6 bude zobrazený následujícími příkazy:
165
// Definice datové třídy pro přenos parametrů TOpenSaveDialog::TData FileData; /* Zápis značí definici proměnné FileData typu třída TData, jež je deklarovaná ve scope TOpenSaveDialog (viz. kap. o objektech). */ // Inicializace členů datové třídy - nutné položky FileData.Flags = // Atributy popisující vlastnosti žádaného souboru a výpisu adresáře. OFN_FILEMUSTEXIST // Povoleno zadat pouze jméno existujícího souboru | OFN_PATHMUSTEXIST // Adresář souboru musí existovat. | OFN_HIDEREADONLY; // Neukazovat ve výpisu soubory s atributem jen číst. FileData.SetFilter("Vše (*.*)|*.*|Texty (*.TXT)|*.TXT|T602 (*.602)|*.602"); /* Filtry pro výpis souborů. Znak | je oddělovačem jednotlivých položek filtru, tvořených páry: Text v nabídce | Přípona filtru - počet filtrů není omezený */ // Nepovinné položky datové třídy - nutné pro náš příklad, ale jinak možno vynechat. FileData.FilterIndex= 2; // Index výchozího zobrazeného filtru, počítáno od 1. strcpy(FileData.FileName, "README.TXT"); /* Výchozí jméno - Pouze do tohoto prvku lze provést strcpy - ostatní se musejí přiřadit = */ FileData.InitialDir= "F:\\TEXT"; // Výchozí adresář, je-li NULL, pak okamžitý. FileData.DefExt= "*.TXT"; /* Přípona, která se má automaticky doplnit ke jménu, pokud není uživatelem zadaná. Je-li NULL, nic se nedoplňuje. */ int iret; // Proměnná pro uložení návratové hodnota dialogu // Vlastní zobrazení dialogu iret=TFileOpenDialog( 0, FileData ).Execute(); // Spuštění dialogu, 0 = není nadřízené okno /* Dialog vrací IDOK, pokud bylo zadán soubor s vlastnostmi určenými FileData.Flags. Jinak nelze volit tlačítko OK. Jméno souboru je uložené ve FileData.FileName. Návratové hodnoty dialogu odpovídají identifikátorům tlačítek (viz. MessageBox)*/ // Použití jména souboru zadaného pomocí dialogu if(iret == IDOK) { FILE * file; file = fopen(FileData.FileName,"rt"); /*... Zpracování souboru */ fclose(file); }
// Otevření souboru.
Poznámka: Položky třídy FileData se rovněž mohly naplnit uvedením v jejím konstruktoru.
7.4 Dialogy a prvky control v objektových knihovnách Objektové knihovny nahrazují dekódování zpráv pro dialog, podobně jako u oken, vytvořením tabulek odezev. OWL zahrnuje třídu TDialog, odvozenou od TWindow, dovolující vytvoření modálního i nemodálního dialogu z resource. Ta nabízí stejné metody jako TWindow a několik málo nových navíc, z nichž nejdůležitější je Execute, ekvivalent Windows API službě ::DialogBox. Třída TDialog má konstruktor: TDialog(TWindow* parent, TResId resId, TModule* module = 0); První a poslední parametr už známe z TWindow; parent, udává nadřízené okno a = 0, pokud dialog nemá majitele; module specifikuje modul a rovná se 0 pro aplikaci. Prostřední argument resId má stejný význam jako obdobné parametry pro nahrávání ikony, menu a akcelerátorů určuje identifikátor resource bloku definujícího dialog.
166
Protějškem uvedené třídy TDialog je v MFC třída CDialog odvozená od CWnd. V té odpovídá metodě Execute metoda DoModal. Pro CDialog se používá několik konstruktorů lišících se způsobem zadání resource (v MFC chybí ekvivalent TResID): CDialog( LPCTSTR lpszTemplateName, CWnd* pParentWnd = NULL ); CDialog( UINT nIDTemplate, CWnd* pParentWnd = NULL ); Ve VCL není žádná specializovaná třída pro dialogy. Prvky control se přidávají do základního okna (třída TForm) a používá se vlastní systém resource. Klasické dialogy lze vytvořit pomocí WinApi funkce CreateDialog, která se probírala v kapitole 7.1. Vytvoření dialogu ze souboru resource představuje sice rychlou metodu, ale na druhou stranu málo flexibilní, protože dialog má předem určený vzhled s pevnou velikostí okna a nedovoluje používat pro prvky různá písma, přidávat k nim kresby a podobně. Rozmanitější grafické podoby lze dosáhnout, pokud se prvky control vytvoří programem a vloží se do okna jako podřízené elementy. Prvky control mají v OWL proto dva konstruktory - jeden pro asociaci s resource, určený pro použití v TDialog, a druhý vhodný pro libovolné okno. Například TListBox má konstruktory: TListBox(TWindow* parent, int resourceId, TModule* module = 0); TListBox(TWindow* parent, int Id, int x, int y, int w, int h, TModule* module = 0); Druhý konstruktor obsahuje místo zadání resource údaje o umístění prvku v oknu a je určený pro uplatnění v konstruktoru objektu okna, kde vytvoří potřebná data dynamickou alokací. Metody nadřízené třídy pak starají o zadaný list box, samy ho vytvoří a zruší. V konstruktorech nejsou atributy uvedené jako volitelné parametry. Ty se nastavují na výchozí hodnoty a nevyhovují-li to, musejí se atributy po alokaci paměti pro prvek upravit. Například TListBox má výchozí atribut LBS_STANDARD, což značí logický součet čtyř atributů: • rámeček (WS_BORDER), • svislá scroll bar (WS_VSCROLL) • abecední třídění (LBS_SORT) • poslat zprávu uživateli při změně (LBS_NOTIFY) Použití TListBox by proto mohlo vypadat následovně: class MojeOkno:TWindow { TListBox * pListBox; // pointer na objekt starající se o list box public: MojeOkno( /*parametry*/ ):TWindow( /*parametry*/ ) // konstruktor MojeOkno { pListBox = new TListBox( // objekt prvku musí mít dynamické duration this, IDC_LISTBOX, // přidělený identifikátor 10,70,200,100 // x, y - levého horního rohu; šířka a výška ); pListBox->Attr.Style & = ~LBS_SORT; // smažeme výchozí bit třídění v atributu pListBox->Attr.Style |= LBS_USETABSTOPS; // přidáme bit použití tabulátorů /* Další operace konstruktoru */ } /* Další deklarace třídy MojeOkno */ }; Úprava dialogu rozebraného v této kapitole na dynamicky vytvořené prvky control by si vyžádala hodně operací. Podobné návrhy se lépe provádějí interaktivně. V Borland C++Builder představuje takový přístup hlavní programovací metodu a velká část zdrojového kódu se vytváří automaticky. Uživatel píše pouze odezvy na zprávy. Poznámka: OWL programy demonstrující použití TDialog a prvků control byly ze skripta vynechané při redukci jeho rozsahu, avšak lze je nalézt na WEB stránce skripta, jejíž URL je uvedená v kapitole 0.
167
7.5 VCL Objektová knihovna VCL, neboli Visual Component Library, se poprvé objevila v roce 1995 jako součást programovacího prostředí Borland Delphi pro jazyk Pascal. Popularita nového produktu, umožňujícího vytvářet aplikace vizuálními interaktivními metodami RAD, Rapid Application Development, přiměla firmu Borland k tomu, aby nad VCL vytvořila i C++ Builder s jazykem C++ kompatibilním s předcházejícím produktem Borland C++. Struktura VCL vychází především z komponent, které mají mnoho společného s prvky control, avšak oproti nim stojí na vyšším vývojovém stupni, především díky uplatnění properties (vlastností) definovaných v Borland C++ Builder jako rozšíření jazyka C++.
7.5a
Properties
Čtení a zápisy proměnných se v objektovém programování často nahrazují voláním metod. To sice markantně zvyšuje bezpečnost, ale na druhou stranu komplikuje zdrojový kód, protože vedle identifikátoru proměnné existují i jména metod pro čtení a zápis. Situaci neřeší ani předefinování operátorů. Dovoluje sice elegantní a bezpečný přístup na data s možností různých formátů vstupu, ale počet operátorů vhodných pro předefinování je omezený a navíc každá větší změna jejich chování působí spíš matoucím dojmem. Properties (vlastnosti, jednotné číslo property) představuje logický krok ve vývoji, řešící problémy práce s daty tím, že dovolují deklarovat abstraktní datový člen třídy. Ten nebude mít přidělenou paměť, jinými slovy při běhu programu nebude neexistovat. Každé jeho použití, jako čtení anebo zápis, přeloží kompilátor voláním metod zadaných při jeho deklaraci. Ve zdrojovém kódu se s property může zacházet jako s kterýmkoliv datovým členem třídy, s jediným omezením - z implementačních důvodů nelze na něj předat referenci, ale naproti tomu, na rozdíl od datového členu třídy, může být i virtuální, neboť představuje funkce. Deklarace property začíná klíčovým slovem __property předřazenému běžné definicí datového členu třídy. V základním tvaru deklarace dále následuje přiřazení příkazového bloku se specifikátory read a write, které popisují identifikátory odpovídajících metod: class PLUSDATA // deklarace třídy { long pamet; // datový prvek pro uložení hodnoty float Cti() { return pamet; }; // metoda pro čtení void Zapis( float hodnota) // metoda pro zápis, s omezením na kladná čísla { pamet = (hodnota<0x7FFFFFFF && hodnota > 0) ? hodnota+0.5 : 0; } public: __property float data = { read = Cti, write = Zapis }; // deklarace abstraktní vlastnosti data }; Třída PLUSDATA obsahuje jediný datový člen pamet typu long. Prvek třídy data představuje abstraktní element, s nímž se bude pracovat jako s číslem typu float. Čtení jeho hodnoty nebude však neznamenat dostup na paměť, ale zavolání funkce Cti. Analogicky tomu, se operace zápisu do prvku data nahradí voláním Zapis. Použití PLUSDATA může vypadat třeba takto: CITAC udaj; // definice objektu udaj udaj.data = 10; // zápis bude interně provedený jako volání udaj.Zapis(10); printf("%f\n", udaj.data); // čtení se interně vykoná jako volání udaj.Cti(); Obě funkce, zadané pro čtení a zápis property, mají předepsané typy a počet argumentů. Čtecí funkce musí vždy vracet typ totožný s typem prvku property a nesmí mít vstupní argument. Inverzně tomu, funkce pro zápis obsahuje jeden argument, typem shodný s typem property, a nevrací žádnou hodnotu. Není však nutné uvést obě funkce. Například lze v deklaraci property vynechat read, čímž vznikne abstraktní člen data, do něhož se dá pouze uložit hodnota, kterou již nelze číst: __property float data = { write = Zapis }; // pouze zápis 168
Třída může mít i více properties. Ukažme si třídu RADIUSVEKTOR pro uložení polárních souřadnic mající dvě properties: /* C++ Builder 3.0 project - Console Wizard */ #include // Definice pro Console aplikace #include <math.h> // matematické funkce #include <stdio.h> // kvůli deklaraci printf class RADIUSVEKTOR { double CtiStupne() { return uhel*180/M_PI; } // čtení úhlu ve stupních void ZapisStupne (double uhelstupne) { uhel = uhelstupne*M_PI/180; } // zápis úhlu ve stupních double CtiX() { return radius*cos(uhel); } // přečti x souřadnici konce radiusvektoru public: double radius; // poloměr radiusvektoru double uhel; // úhel v radiánech radiusvektoru __property double stupne = { read=CtiStupne, write=ZapisStupne }; // lze zapisovat i číst __property double x = { read= CtiX }; // property x lze pouze číst RADIUSVEKTOR(double r, double uhelstupne) { radius = r; stupne = uhelstupne; } }; void main(void) { RADIUSVEKTOR rvektor(1,45); // vytvoříme objekt rvektor s délkou 1 a s úhlem 45° printf("%lg\n", rvektor.stupne); // vytištěno 45, vykonalo se rvektor.CtiStupne() rvektor.stupne=60; printf("%lg\n", rvektor.x); // rvektor.x=10; rvektor.uhel=M_PI; printf("%lg\n", rvektor.stupne);
// // // // //
uložíme 60°, čili se provede rvektor.ZapisStupne(60); vytištěno 0.5, k čtení x byla použitá metoda CtiX člen x není dostupný pro zápis, lze ho pouze číst přímý zápis úhlu v radiánech vytištěno 180; ke čtení byla použitá metoda CtiStupne
} Poznámky: • Uvedená deklarace properties představuje základní variantu. Jazyk Borland C++ Builder dovoluje další možnosti, například definice array properties, jejíž read a write funkce navíc dostávají index. Kromě toho existují i jiné specifikátory, vesměs již vztažené k samotnému procesu překladu. Blížeji o nich v následující kapitole.
7.5b
Komponenty
Komponenty lze v prvním přiblížení považovat za třídy oken rozšířené o možnost properties. Ptáte-li se, proč se pro takovou drobnou úpravu, jako přidání možnosti překladu čtení a zápisu dat funkcemi, zavádí nový název, pak to lze vysvětlit tím, že properties představují nepatrný zásah do stylu deklarací objektů, ale obrovskou změnu ve svých důsledcích. Prvky control, o nichž jsme hovořili v předchozí části, skýtaly výhodu tvorby grafického prvku z předdefinovaných elementů. Jejich praktičtějšímu použití však bránila nejednotnost jejich řízení. Například, velikost tlačítka se při jeho vytvoření zadávala v argumentech konstruktoru TButton, ale po jeho vzniku se další změny vzhledu už prováděly službami OS - GetWindowRect a MoveWindow, obě se vzájemně lišící tvarem parametrů. Properties dovolují sjednotit různorodost pomocí definic abstraktních datových elementů pro umístění a rozměry tlačítka. Metody s nimi spojené podle situace provedou čtení nebo zápis pomocí adekvátní operace. Navíc se properties, za jistých podmínek, dají v Borland C++ Builder specifikovat jako použitelné už v okamžiku vytváření programu (compile-time) klíčovým slovem __published. Hodnoty takových properties se při psaní zdrojového kódu zobrazují formou seznamu. Mohou mít výchozí hodnoty určené specifikátorem default a dají se při psaní zdrojo169
vého kódu zadávat vyplněním tabulky, podle níž programovací prostředí mění odpovídající příkazy v souborech projektu. Podobným způsobem se v RAD také definují zprávy, které jednotlivé komponenty posílají svému nadřízenému oknu a na jejichž základě se vyvolávaly metody obsluhující událost event-handlers. Třeba nabídka komponenty TButton, od níž byl vytvořený děděním objekt s identifikátorem OK, vypadá v době psaní programu takto:
Obr. 7-7 Nabídka _published properties a event-handlers pro komponentu tlaèítka Nabídka obsahuje řadu údajů klasického tlačítka, jehož je komponenta rozšířením, jako například umístění levého horního rohu a šířka a výška tlačítka - Left,Top a Width, Height; nebo titulek - Caption. Ty jsou doplněné dalšími možnostmi - chování tlačítka při tažení nějakého objektu myší (drag), nabídkou textu, který se objeví jako nápověda - hint; zadání typu písma font; a jinými. Rovněž se rozšířila i nabídka zpráv. Prvek control měl zprávu BN_CLICKED; ta je i v komponentě jako OnClick a bude se obsluhovat metodou CmStiskOK. Komponenta však kromě ní navíc zpracovává i další zprávy související s pohybem myši.
7.5c
Vlastnosti a struktura VCL
Vzhledem k tomu, že práce s Borland C++ Builder a knihovna VCL budou hlavními tématy skripta určeného pro cvičení z tohoto předmětu, uvedeme pouze prvky, které se projeví při konverzi předchozího příkladu, řešeného pro OWL i MFC, do VCL, a vynecháme hlubšího vysvětlení jednotlivých objektů. 170
Řada VCL tříd má identifikátory totožné s OWL, avšak jejich chování se v mnohém změnilo, aby se hodilo pro RAD. Ze stejného důvodu se zjednodušila i struktura oken. OWL deklarovala dvanáct tříd pro tvorbu oken, lišící se svoji různou specializací, z nichž jsme se zmínili pouze o dvou nejčastějších - o základním oknu TWindow a oknu TFrameWindow s podporou menu. Naproti tomu, o VCL lze tvrdit, pomineme-li pomocné speciální objekty, že zahrnuje pouze jednu obecnou třídu okna - TForm. Do té se vkládají komponenty jako její podřízená okna. "$ VCL zahrnuje kolem sta komponent; počet závisí na verzi Borland C++ Builder. Uživatel má dále možnost vytvářet si vlastní komponenty, při zachování plnohodnotné nabídky služeb pro ně, což se vtahuje i na možnost __published properties a zpráv zadávaných při psaní programu pomocí tabulek, pomocí maker podobně jako v OWL. VCL nabízí komponenty pro veškeré prvky control uvedené v tabulce na straně 162. Údaje o poloze, atributech prvků a zprávách se ukládají do automaticky vytvářených souborů projektu a využívají se při překladu. Některé elementy popisují abstraktní komponenty. Například menu už nevytváří v grafickém editoru, ale komponentou TMainMenu, která se vloží jako člen do třídy okna odvozené děděním od TForm. Jednotlivé položky menu se pak přidávají vkládáním komponent TMenuItem, které slouží jednak pro jejich definování a jednak pro ovládání jejich vzhledu při běhu programu, viz. příklad na konci této kapitoly. Změnilo se i kreslení do okna, dříve prováděné přes DC, device context. Třída TForm nyní obsahuje jako svůj člen objekt Canvas, typu třídy TCanvas (canvas = malířské plátno). Canvas se vytváří automaticky v TForm a přes něj jsou dostupné veškeré služby prováděné dříve prostřednictvím DC, plus některé navíc. Canvas se od DC liší však v tom, že obsahuje jako svoje prvky veškeré grafické objekty - jako Font, Brush, Color, Pen. Ty mají jako svoje vlastnosti určené pomocí property. Například psaní červeným fontem Arial CE se provede takto: Canvas->Font->Color= (TColor) RGB(jasbarvy, 0, 0); // Změna property Color - barva fontu Canvas->Font->Name = "Arial CE"; // Změna poperty jména fontu Canvas->Font->Size = 12; // Změna property velikost fontu v bodech Z Windows API víme, že font vzniká jako celek. Dá se změnit barva psaného textu, ale nelze u jednou vytvořeného fontu změnit velikost nebo typ. Canvas to však dovoluje. Měníme vlastnosti fontu, bude to mít za následek zrušení předchozího fontu a vytvoření nového. Záleží pouze na implementaci objektu Canvas, kdy k tomu dojde. Velikost fontu se tentokrát zadává v typografických bodech (tiskařsky je 12 bodů = 1 cicero a 6 cicero = 1 palec), zatímco API se určovala v interních logických jednotkách Windows, které se musely přepočítávat podle konkrétních rozměrů periférie, na níž se právě kreslilo. K další změně došlo v předávání parametru metodám obsluhujícím zprávy. Ty dostávají místo na zprávě závislých dat pointer na univerzální objekt "TObject *Sender", nesoucí informaci o objektu, z něhož byla zpráva zaslaná. Tento krátký přehled zakončíme přehledem odlišností zacházení s VCL oproti OWL (více v připravovaném skriptu na cvičení): • VCL nedovoluje přetěžování metod - tato skutečnost zjednodušuje analýzu kódu a dovoluje jeho automatickou správu. O použité metodě lze rozhodnout okamžitě, zatímco přetěžování vyžaduje znalost typů proměnných, což se obvykle ví až při překladu; • metody objektů VCL nepoužívají výchozí (default) parametry, kvůli stejným důvodům jako přetěžování; • objekty VCL nedovolují vícenásobnou dědičnost, tzn. dědit jednu třídu od více tříd; což moc nevadí, podobné operace se stejně používají ojediněle a dají se vždy nahradit; "$
Podstata skoro každého grafického prvku Windows vychází z okna, včetně VCL komponent, protože okna mohou přijímat zprávy prostřednictvím callback funkcí a OS pro ně nabízí grafickou podporu.
171
• objekty VCL provádějící grafické operace se musejí vytvářet vždy jako dynamické. Lze třeba mít statický TRect, protože ten obsahuje pouze data, ale objekt typu TPen musí vzniknou operátorem new. Důvodem leží ve skutečnosti, že knihovna VCL byla celá napsaná v jazyce Pascal (byla původně vytvořená pro Delphi) a dynamické alokace new dovolují její snadnější propojení. Doufám, že jste se teď nezhrozili - Pascal a jazyk C++, jak to vlastně jde dohromady? Docela dobře, procesoru počítače je to úplně jedno; oba překladače produkují jeho strojový kód. Naopak, podobné spojení skýtá velkou výhodu v dobře odladěné knihovně VCL (ta se používá ve dvou produktech a tím se prověřuje hned dvojnásobně) a současně v síle jazyka C++. Uvedená omezení se totiž týkala výhradně objektů definovaných VCL. Vlastní třídy dovolují veškeré prostředky, které C++ objektové programování nabízí, včetně přetěžování metod i operátorů a další specialit, o nichž jsme hovořili v tomto skriptu. To je také důvod, proč jsme výklad začínali od OWL, ta nastínila možnosti použití objektů. Kromě toho, si vždy lze vzít Windows API handle nějakého objektu, jako třeba si od Canvas vyžádat handle na vytvořené DC a poté pracovat se službami Windows API, pokud se nám bude nabídka metod VCL zdát příliš chudá.
7.5d
Pøíklad VCL programu
Příklad řešený pomocí Windows API, OWL a MFC by v Borland C++ Builder vypadal takto: Zdrojový kód psaný uživatelem má běžnou velikost Arial Narrow fontu, přičemž příkazy totožné se zdrojovým kódem OWL se uvádějí normálním písmen a změněné příkazy tučným písmem. Části zdrojovém kódu, které programovací prostředí vygenerovalo automaticky, označuje kurzíva a menší písmo. Byly ručně přeformátované tak, aby zabraly co nejméně řádek. Tyto příkazy nejsou převážně komentované .
/**************** MinCB.CPP ******************/ #include #pragma hdrstop USERES(" MinCB.res "); USEFORM("Main.cpp", MainForm);
// uživatelovo resource obsahující ikonu
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TMainForm), &MainForm); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; }
/* -------------- Main.H - z větší části generovaný automaticky ---------------- */ #ifndef MainH #define MainH
/* Automaticky generovaný blok include příkazů - znaky | označují konce řádek */ #include #include #include #include #include
| | | | |
#include #include #include #include #include
class TMainForm : public TForm { __published: TMainMenu * MainMenu1 ;
| | | | |
#include #include #include #include #include <Menus.hpp>
// Uživatel psal pouze identifikátory položek TMenuItem * Soubor1 ; TMenuItem * CmKonecMenuItem ; TMenuItem * Obrzek1 ; TMenuItem * CmCtverecMenuItem ; TMenuItem * CmElipsaMenuItem ; TMenuItem * N1; // oddělovací čára v menu TMenuItem * CmJasMenuItem ; TMenuItem * CmJasMinusMenuItem ; TMenuItem * N2; // oddělovací čára v menu TMenuItem * CmSmazatMenuItem ;
172
/* Metod pro obsluhu událostí; uživatelem byly psané pouze identifikátory metod */ void __fastcall EvPaint (TObject *Sender); void __fastcall CmKonec (TObject *Sender); void __fastcall CmCtverec (TObject *Sender); void __fastcall CmElipsa (TObject *Sender); void __fastcall CmJas (TObject *Sender); void __fastcall CmJasMinus (TObject *Sender); void __fastcall CmSmazat (TObject *Sender); void __fastcall EvSize (TObject *Sender); private:
// private user declarations
/* Data a metody BOOL ctverec; BOOL elipsa; BYTE jasbarvy;
uživatele s atributem přístupu private vložené ručně. */ // zobrazený čtverec // zobrazená elipsa // jas barvy 0..255
void Nuluj() { ctverec = elipsa = FALSE; jasbarvy = 128; } public: // public user declarations virtual __fastcall TMainForm(TComponent* Owner); }; extern PACKAGE TMainForm *MainForm; #endif
/* Main.CPP * obecné deklarace generované automaticky, kódy metod dopisované ručně */ #include #pragma hdrstop #include "Main.h" #pragma resource "*.dfm" TMainForm *MainForm; __fastcall TMainForm::TMainForm(TComponent* Owner) : TForm(Owner) {
Nuluj(); }
void __fastcall TMainForm::EvPaint(TObject *Sender)
{ char * text="Střed"; int xs, ys; xs=ClientWidth/2; ys=ClientHeight/2; ::SetBkMode( Canvas->Handle, TRANSPARENT); if(elipsa) { Canvas->Brush->Color=(TColor) RGB(0, 0, jasbarvy); Canvas->Brush->Style=bsSolid; Canvas->Pen->Color=Canvas->Brush->Color; Canvas->Pen->Width=0; Canvas->Pen->Style=psSolid; Canvas->Ellipse(0,0, ClientWidth, ClientHeight); } if(ctverec) { Canvas->Brush->Color=(TColor) RGB(0, jasbarvy, 0); Canvas->Brush->Style=bsDiagCross; Canvas->Pen->Color=Canvas->Brush->Color; Canvas->Pen->Width=3; Canvas->Rectangle(xs-100, ys-100, xs+100, ys+100); }
173
// vypisovaný text // získáme střed okna /* kresba transparentně - použit Windows API příkaz */ // modrá barva // jednolitá výplň // nepřerušovaná čára // elipsa
// zelená barva // křížové šrafování // síla pera tři // čtverec
// Psaní textu do středu okna Canvas->Font->Color= (TColor) RGB(jasbarvy, 0, 0); Canvas->Font->Name = "Arial CE"; Canvas->Font->Size = 12; ::SetTextAlign(Canvas->Handle, TA_CENTER | TA_BASELINE); Canvas->TextOut(xs, ys, text);
// // // // //
červená barva jméno fontu pro psaní velikost v bodech umístění textu - služba API napsání textu
} void __fastcall TMainForm::CmKonec(TObject *Sender) { void __fastcall TMainForm::CmCtverec(TObject *Sender { CmCtverecMenuItem->Checked = ctverec =
Close(); } !ctverec; Invalidate(); }
void __fastcall TMainForm::CmElipsa(TObject *Sender) { CmElipsaMenuItem->Checked = elipsa = !elipsa;
Invalidate(); }
void __fastcall TMainForm::CmJas(TObject *Sender) { if(jasbarvy<240) jasbarvy += 16; else jasbarvy=255;
Invalidate(); } void __fastcall TMainForm::CmJasMinus(TObject *Sender) { if(jasbarvy>=16) jasbarvy-=16; else jasbarvy = 0;
Invalidate(); } void __fastcall TMainForm::CmSmazat(TObject *Sender) { Nuluj(); CmElipsaMenuItem->Checked = elipsa;
CmCtverecMenuItem->Checked = ctverec;
Invalidate(); } void __fastcall TMainForm::EvSize(TObject *Sender) {
Invalidate(); }
Poznámky ke zdrojovému kódu: • Zdrojový kód nestačí k přeložení aplikace. K tomu jsou potřeba ještě soubory projektu, v nichž se nacházejí některé klíčové informace - například tabulky zpráv, vzhled resource. Jedná se o automaticky generovaná data ukládaná v netextové podobě. Úplný příklad lze nalézt na WEB stránce skripta (viz. kapitola 0). Tam budou časem umístěné i další výukové příklady pro Borland C++ Builder. • Úplné délky souboru aplikace pro jednotlivé verze příkladu, tj. bez použití jakýchkoliv externích součástí - překlad byl provedený pro statické run-time knihovny, bez informace pro debugger (u OWL) a bez použití externích modulů, packages (u Borland C++ Builder): Windows API OWL MFC VCL
49 143 147 249
kB kB kB kB
1.00 x 2.91 x 3.00 x 5.08 x
0.34 x 1.00 x 1.02 x 1.74 x
Co se týká délky souboru aplikace, umístila se VCL na posledním místě, díky svým příliš univerzálním RAD třídám, zatímco Windows API suverénně vyhrálo. Ovšem, kdyby se místo délky výsledného souboru aplikace měřil čas nutný k napsání zdrojového kódu, pořadí by se převrátilo. VCL by předběhla ostatní nástroje o mnoho člověko-hodin, dnů či měsíců a Windows API by dýchavičně zůstalo na samém konci. RAD metody, jak už napovídá jejich název, totiž minimalizují především dobu nutnou k vytvoření aplikace.
174
8 Zvláštnosti programů ve Win32 Před čtením této pasáže doporučuji čtenáři osvěžit si pojmy uvedené v kapitolách 1.3 a 4.1 .
8.1
Charakteristiky Win32
Z pohledu programátora lze rozlišit několik 32 bitových prostředí: Win32s - Win16 rozšířené o 32 bitovou adresaci, avšak bez podpory dalších služeb 32 bitových systémů, dnes zajímavé jen pro Win16. Win32 - 32 bitový operační systém vyskytující se ve dvou provedeních odlišných vnitřní architekturou: ♦ Windows 95/98 (dále označované jen jako Windows 95), které nabízejí pro 32 bitové aplikace preemptivní multitask, vlákna (threads) a virtuální správu paměti na úrovni Windows NT a to včetně jejich stability, avšak 16-ti bitové aplikace spouštějí v nestabilním módu Win16 kvůli zpětné kompatibilitě. ♦ Windows NT - 32 bitový operační systém rozšířený o řadu možností, avšak díky tomu kladoucí větší nároky na hardware počítače. Jeho jádro umožňuje oproti Windows 95 především zabezpečení proti narušení systému a úniku dat, s čímž souvisí i víceuživatelské prostředí. Všechny aplikace se spouštějí v preemptivním multitask-u a podporuje se symetrická víceprocesorová architektura, dovolující rozklad úloh teoreticky až na 32 procesorů, ale prakticky jen na dva (grafika a zbylé operace), protože s výjimkou specializovaných úloh je efektivita využití dalších procesorů v běžných aplikacích velmi nízká. Jednotlivé OS Windows se výrazně liší svoji implementací, avšak té nebudeme věnovat pozornost, protože jednak o ní pojednávají jiné publikace a jednak nemá větší vliv na kód programu - ten závisí především na službách nabízených uživateli, tj. na Windows API. Z jejich pohledu lze říct: ♦ všechna Windows mají podobná API rozhraní shora velmi kompatibilní s Win16. Výjimky tvoří odlišné parametry některých funkcí a zpráv, díky přechodu ze 16 na 32 bitů, a také vynechání služeb Win16 příliš svázaných s MS-DOSem ve Win32. ♦ Windows NT jsou shora téměř kompatibilní s Windows 95. Slovo téměř se vztahuje na vynechání některých funkcí ponechaných ve Windows 95 kvůli programům napsaným pro Win16, zejména týkající se přímého přístupu na hardware, a drobné odlišnosti v chování některých služeb, viz. dále. Windows 95 se svým vzhledem a činností hodně přiblížily k Windows NT, ale některé možnosti jim stále chybějí. Uvedeme pouze ty nejdůležitější diference: Jádro - Windows 95 a NT se liší výrazně jeho implementací. Ta však nemá větší vliv na kód programu, s výjimkou několika věcí: ♦ chybové kódy (GetLastError) se ve Windows 95 mohou lišit od Windows NT díky jiné vnitřní implementaci. ♦ jádro Windows 95 dovoluje asynchronní činnost (overlapped mode) pouze pro sériové vstupy a výstupy; neumožňuje ji pro soubory. ♦ funkce data FileTimeToDosDateTime a DosDateTimeToFileTime jsou ve Windows 95 omezené na maximální datum 31.12.2099, zatímco Windows NT mají maximum 31.12.2107 ♦ sdílené paměťové soubory, viz. dále v této kapitole, se objeví na totožné adrese pro všechny aplikace.
175
Grafika - vykazuje největší rozdíly mezi oběma 32 bitovými OS: ♦ Windows 95 používají pro grafické rozhraní GDI pouze 16-ti bitové souřadnice. Grafické služby mají sice 32 bitové parametry, kvůli kompatibilitě, ale horních 16 bitů se prostě ignoruje. ♦ OpenGL 3-D grafika Windows NT, určená pro prostorová zobrazení, není implementovaná ve Windows 95, což je přímým důsledkem předchozího bodu. ♦ Čárkované a tečkované čáry smějí ve Windows 95 být síly pouze 1. ♦ Windows 95 nepodporují rozšířený grafický mód umožňující interní transformace souřadnic rotací a zkosením. ♦ Služba DeleteObject se chová odlišně v situaci, že dojde k pokusu zrušit grafický objekt, který se používá díky SelectObject pro DC (device context). Ve Windows 95 dojde ke zrušení objektu a jeho následném vyřazení z použití v DC, zatímco v Windows NT se objekt vůbec nezruší a DeleteObject vrátí FALSE. ♦ Některé služby tisku nejsou pod Windows 95 dostupné kvůli odlišnému provedení obsluhy tiskáren. ♦ Záznam grafické cesty službami BeginPath a EndPath ve Windows 95 registruje pouze následující funkce CloseFigure, ExtTextOut, LineTo, MoveToEx, PolyBezier, PolyBezierTo, Polygon, Polyline, PolylineTo, PolyPolygon, PolyPolyline a TextOut. Okna - liší se v horních limitech některých prvků: ♦ 16-ti bitová interní správa oken ve Windows 95 dovoluje vytvořit maximálně 16364 oken "%, což sice může připadat jako hodně, ale nutno si uvědomit, že oknem je každý prvek control, tedy každé tlačítko, scroll bar apod. ♦ Windows 95 limitují obsah některých prvků control, například elementy založené na list box smějí mít pouze 32767 položek; ediční pole dovoluje pouze 64 kilobytů textu apod. ♦ Windows 95 mají pouze jeden desktop. Ochrana systému - ve smyslu proti narušení či úniku dat chybí ve Windows 95. V nich není implementovaná bezpečnost objektů, která dovoluje ve Windows NT přiřadit přístupová práva kontrolovaná podle přihlášení uživatele. Odpovídající parametry se sice vyskytují ve funkcích kvůli kompatibilitě, ale jsou ve Windows 95 ignorované. Například v CreateFile smí místo příslušného oprávnění mít NULL. Unicode - tj. 16-ti bitový kód znaků dovolující používat všechna světová písma není ve Windows 95 příliš podporovaný. Existují sice některé konverzní funkce mezi Unicode a ASCII a základní výstupy v Unicodu, ale pouze ve velmi omezené. Srovnání Win16 s Win32 by vy adovalo mnohem delší popis. Zmíníme se jen o nejvìtších zmìnách, z nich o mnohých se ji hovoøilo v pøedchozích kapitolách: Na rozdíl od Win16 platí pro Win32: ♦ ka dá aplikace pracuje v pamì ovém modelu flat a má 4Gbyte virtuální pamìti; ♦ vše má 32 bitù s výjimkou char a BYTE (8), short a WORD (16) a myši, která vrací stále jenom 16 bitové souøadnice; ♦ ikony 16x16 dole na liště, 32x32 na pracovní ploše, tzn. aplikace potřebuje pro hlavní ikonu její dva obrazy různé velikosti. Pokud chybí větší či menší protějšek, Win32 ho odvodí vynecháním nebo přidáním bodů, avšak výsledek většinou nevypadá dobře; ♦ vestavìná mo nost Console aplikací; ♦ obsluha COM portů se provádí přes CreateFile, zcela chybějí Win16 funkce OpenComm, ReadComm, WriteCom a podobně. "%
Nejde o překlep - počet oken není 16384, ale skutečně jen 16364.
176
♦ existují pipes (potrubí) - umožňující propojit dvě aplikace na jednom počítači na principu podobném sériovému kabelu, avšak řešeném softwarově. ♦ OLE 2 - významný prvek dovolující používání služeb jiných aplikací; ♦ jména souborù dlouhá a 255 znakù vèetnì mezer; ♦ systémový registr harwaru a aplikací; ♦ mo nost více vláken, multithread; ♦ vestavìné strukturované výjimky, structured exceptions handling; ♦ virtuální alokace pamìti; ♦ sdílení paměti mezi aplikacemi je možné pouze přes mapování souborů. V této kapitole se zmíníme pouze o posledních šesti bodech, zvýrazněných tučně. Ty patří mezi přední přínosy Win32 a přitom se dají vyložit na omezeném rozsahu skript. To bohužel neplatí o OLE 2, významném ale komplikovaném prvku, jehož objasnění by si vyžádalo samostatnou publikaci. Jeho stručná charakteristika bude uvedená v poslední kapitole skripta, spolu s údaji o jeho předchůdci, DDE protokolu, určeném pro přenos dat, který má význam pro řízení. Ve výkladu vynecháme také pipe, které představují sice zajímavý element, ale nahraditelný jinými prvky.
8.2 Soubory s dlouhými jmény Použití dlouhých jmen nečiní obtíže. Se soubory lze pracovat běžnými knihovními funkcemi: #include <stdio.h> #include <windows.h> #include void main(void) // Vstupní bod Console aplikace pro Win32 { char buf[256]; char * sFileName = "Soubor s dlouhym jmenem.txt"; // Jméno souboru FILE *fp; if ((fp = fopen(sFileName, "rt")) != NULL) { while(!feof(fp)) { fgets(buf,sizeof(buf),fp); printf("%s",buf); } } else printf("Soubor nenalezen.\n");
// Práce se soubory FILE pohodlnější // Dokud není konec souboru, // čti a vypisuj řádku
} Práce s dlouhými jmény vyžaduje jen vyhrazení dostatečně velkých proměnných. Program stanovení jména souboru nápovědy, který je uložený v adresáři aplikace a liší se od jejího názvu jen příponou HLP, se ve Win16 dá napsat takto: #include int main(int argc, char * argv[]) { char drive[3]; // disk, např. "C:" char dir[80]; // adresář bez disku zakončený \, např. "\\Program\\" char file[9]; // jméno souboru bez přípony, např. "POKUS" char ext[5]; // přípona začínající tečkou, např. ".EXE" char helpfile[80]; // jméno help souboru fnsplit(argv[0], drive, dir, file, ext ); // rozděl úplné jméno aplikace na jeho složky fnmerge(helpfile,drive,dir,file,"HLP"); // spoj složky ve jméno souboru nápovědy /* další operace */; return 0; } Naproti tomu Win32 vyžadují mnohem delší řetězce a to včetně přípony!
177
#include int main(int argc, char * argv[]) { char drive[3]; char dir[260]; char file[260]; char ext[260]; char helpfile[260];
// nemění se pouze délka označení disku
// jméno help souboru
fnsplit(argv[0], drive, dir, file, ext ); // rozděl úplné jméno aplikace na jeho složky fnmerge(helpfile,drive,dir,file,"HLP"); // spoj složky ve jméno souboru nápovědy /* další operace */ ; return 0; } Délky se často definují systémovými konstantami, čímž se program stane nezávislým na překladu. Potřebné změny zařídí předdefinovaná makra v include souborech. Překladače Borland je mají v dir.h spolu s deklaracemi fnsplit a fnmerge: #include int main(int argc, char * argv[]) { char drive[MAXDRIVE]; char dir[MAXDIR]; char file[MAXFILE]; char ext[MAXEXT]; char helpfile[MAXPATH]; // jméno help souboru fnsplit(argv[0], drive, dir, file, ext ); // rozděl úplné jméno aplikace na jeho složky fnmerge(helpfile,drive,dir,file,"HLP"); // spoj složky ve jméno souboru nápovědy /* další operace */; return 0; } Poznámka: Prostředí Visual C++ má odpovídající konstanty: _MAX_DRIVE, _MAX_DIR, _MAX_FNAME, _MAX_EXT, _MAX_PATH definované v stdio.h, a funkci pro rozdělení jména na složky _splitpath, k opačnou pak _makepath, obì definované v stdlib.h a mající totožné argumenty včetně jejich pořadí, jako zmíněné fnsplit a fnmerge. Standardní funkce pro práci se soubory nedovolují využít všech možností Win32. Ty jsou dostupné pouze po otevření souboru službou CreateFile: HANDLE CreateFile( LPCTSTR lpFileName, // jméno souboru DWORD dwDesiredAccess, // charakter přístupu (čtení-zápis) DWORD dwShareMode, // mód sdílení souboru LPSECURITY_ATTRIBUTES lpSecurityAttributes, // bezpečnostní atributy DWORD dwCreationDistribution, // jak vytvořit DWORD dwFlagsAndAttributes, // atributy souboru HANDLE hTemplateFile // handle souboru, z něhož se mohou okopírovat atributy ); Parametry mají následující význam: lpFileName - jméno souboru, nebo komunikačního zdroje (COM1, COM2) dwDesiredAccess - požadovaná přístupová práva, kombinace atributů: 0 - pouze možnost dotazu na stav bez zápisu a čtení GENERIC_READ - čtení souboru GENERIC_WRITE - zápis do souboru dwShareMode - udává možnost sdílení souboru, kombinace atributů 0 - nelze sdílet 178
FILE_SHARE_READ - ostatní operace smějí soubor pouze číst FILE_SHARE_WRITE - ostatní operace smějí do souboru zapisovat lpSecurityAttributes - adresa bezpečnostní atributů, minimální verze viz příklad. dwCreationDistribution - udává povolené akce při otevírání souboru CREATE_NEW - nový soubor a hlásit chybu, pokud nějaký existuje CREATE_ALWAYS - vždy nový soubor, přepsat případný starý. OPEN_EXISTING - otevřít existující soubor a chyba pokud není. OPEN_ALWAYS - otevřít pokud existuje a když není, pak provést CREATE_NEW. TRUNCATE_EXISTING - otevřít soubor a zkrátit jeho délku na 0 bytů. Hlásit chybu, pokud soubor neexistuje. Soubor musí mít přístup GENERIC_WRITE. dwFlagsAndAttributes - běžné atributy souboru, buď použito FILE_ATTRIBUTE_NORMAL, tj. žádné atributy, nebo kombinace běžných atributů souborů známých z MS-DOSu: FILE_ATTRIBUTE_ARCHIVE , FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM k tomu přibývají navíc atributy určující chování OS k danému souboru: FILE_FLAG_WRITE_THROUGH - okamžitě zapisovat, bez cashe. FILE_FLAG_OVERLAPPED - asynchronní operace běžící na pozadí, jejichž konec je nahlášen událostí nebo se dá zjistit zavoláním GetOverlappedResult. Tento bit dovoluje provádět několik operací se souborem paralelně. Je-li zadán, pak služby ReadFile a WriteFile musejí vždy využívat OVERLAPPED. Ve Windows95 lze overlapped použít pouze pro sériové porty. FILE_FLAG_NO_BUFFERING - specifikuje, že se pro soubor nemá používat vyrovnávací paměť. V tom případě se soubor musí číst po sektorech, protože disk to jinak neumí. Má-li jeho sektor 1024 bytů, pak z něho lze přečíst 1024, 2048, 4096 bytů, ale už ne 256 či 1111 bytů. Velikost sektoru se zjistí GetDiskFreeSpace. FILE_FLAG_RANDOM_ACCESS - udává, že bude použitý náhodný přístup. OS použije informaci pro optimalizaci vyrovnávací paměti FILE_FLAG_SEQUENTIAL_SCAN - udává, že se soubor bude číst postupně, opět význam pro optimalizaci přístupu. FILE_FLAG_DELETE_ON_CLOSE - dočasný soubor, který OS smaže ihned po uzavření všech handle, které s ním pracují. Užití CreateFile je rovněž nezbytné pro zjištění času uložení souboru, protože ve Win32 neexistují již odpovídající funkce z Win16. Následující příklad Console aplikace vypíše čas posledního zápisu do souboru: #include <stdio.h> #include <windows.h> #include void main(void) // Vstupní bod Console aplikace pro Win32 { char buf[256]; char * sFileName = "Soubor s dlouhym jmenem.txt"; // Jméno souboru SYSTEMTIME st; FILETIME ft;
// čas rozdělený na složky // čas v souboru
(Win16: struct time st; struct date sd;) (Win16: struct ftime ft;)
SECURITY_ATTRIBUTES ofs; // Bezpečnostní atributy ( ve Windows 95 ignorované) ofs.nLength = sizeof(SECURITY_ATTRIBUTES); // Nutno vyplnit délku struktury! ofs.lpSecurityDescriptor = NULL; // Atribut sdílení souboru, NULL = základní ofs.bInheritHandle = FALSE; // Handle není možné zdědit od jiného procesu
179
HANDLE hFile; // Handle otevřeného souboru hFile = ::CreateFile( // Vlastní otevření souboru sFileName, // Jméno souboru GENERIC_READ, // Přístup - pouze čtení FILE_SHARE_WRITE | FILE_SHARE_READ, // Možno sdílet pro čtení i zápis &ofs, // Adresa bezpečnostního deskriptoru OPEN_EXISTING, // Otevřít výhradně existující soubor, nevytvářet ho. FILE_ATTRIBUTE_NORMAL, // Normální atributy souboru 0 // Ze kterého souboru okopírovat atributy - 0 = ze žádného ); if(hFile != INVALID_HANDLE_VALUE) { GetFileTime(hFile, NULL, NULL, &ft); // Čas v souboru (Win16: getftime(fileno(fp), &ft); ) FileTimeToSystemTime(&ft,&st); // Převedeme čas na jeho numerické složky printf("Soubor: %2d:%02d:%02d %02d.%02d.%4d [d.m.y]\n", st.wHour,st.wMinute,st.wSecond,st.wDay,st.wMonth,st.wYear ); CloseHandle(hFile); } else printf("Soubor nenalezen.\n"); while(!kbhit());
// Zavřeme soubor
// Počkat na stisk klávesy před zavřením Console okna
} Všimněte zapsání délky struktury do jejího členu nLength. Podobný přístup bývá ve Win32 běžný. Skoro každá systémová struktura pro přenos parametrů obsahuje prvek udávající její délku, který musí před použitím nastavit operací sizeof. Soubor otevřený CreateFile lze zpracovávat Win32 operacemi pro čtení a zápis, ReadFile a WriteFile. Ty mají sice o něco jednoduší tvar než CreateFile, ale stále komplikovanější než knihovní funkce jazyka C. Poznámka 1: Ve Win32 se čte jiným způsobem i čas počítače: SYSTEMTIME st; // čas rozdělený na složky (Win16: struct time st; struct date sd;) GetLocalTime(&st); // přečti čas počítače. (Win16: gettime(&st); getdate(&sd);) Poznámka 2: CreateFile umožňuje vytvořit handle na komunikační zařízení jako COM1, COM2. V tom případě dwCreationDistribution parametr se musí rovnat OPEN_EXISTING a hTemplate konstantě NULL. Pro porty lze specifikovat přístup dwDesiredAccess typu čtení, zápis, nebo čtení a zápis. Atributy mohou udávat overlapped operaci. Funkce pro nastavení portů začínají SetComm..., respektivě GetComm... , a na změny řídících bitů portů lze čekat pomocí WaitCommEvent. Poznámka 3: CreateFile dovoluje pracovat i s klávesnicí; její jméno je "CONIN$". Aplikace Console smějí otevřít handle také na vyrovnávací paměť výstupu na obrazovku. Ten má jméno "CONOUT$".
8.3 Systémový registr Registrační informace jsou ve Win32 soustředěné do dvou skupin: • klasické *.INI soubory - s nimiž lze manipulovat skupinou funkcí začínající GetPrivateProfile... a WritePrivateProfile... (respektivě pro WIN.INI GetProfile... a WriteProfile...), například WritePrivateProfileString, GetPrivateProfileInt, GetProfileSection. • systémový registr, který představuje jednolitý soubor společný všem programům.
180
Soubor obsahují sekce, uzavřené do hranatých závorek, a pod nimi mají uložené jednotlivé identifikátory položek, označovaných jako klíče. Každý klíč má přidělenou hodnotu. Se soubory *.INI se pracuje oddělenými příkazy. Každý z nich otevře žádaný soubor, provede s ním požadovanou operaci a soubor opět uzavře, například: WritePrivateProfileString( // Zapiš do souboru "FileNames", // Jméno sekce v souboru "LastDataFile", // Jméno klíče uvnitř sekce "MojeData.dat", // Hodnota přiřazená klíči "F:\\PJR\\Program.ini" ); // Soubor *.INI, s nímž se pracuje Popsaný program by zapsal v souboru F:\PJR\Program.ini do sekce [FileNames] klíč LastDataFile a k němu odpovídající hodnotu MojeData.dat: ;předchozí sekce a klíče [FileNames] LastDataFile=MojeData.dat ;další klíče v sekci [FileNames] přičemž by se vytvořil soubor F:\PJR\Program.ini, pokud by neexistoval; zapsal by se titulek sekce [FileNames], kdyby nebyl v souboru dosud zanesený; zaznamenal by klíč LastDataFile, kdyby ho sekce ještě neobsahovala, a ke klíči by se připojila hodnota MojeData.dat. Opačná funkce GetPrivateProfileString by hodnotu u klíče opět přečetla. Aplikace pro Win32 dávají přednost systémovému registru. Práce s ním liší od souborů *.INI jiným charakterem přístupu. Registr se vyznačuje víceúrovňovou strukturou připomínající "& diskový adresář . Jeho položkám se místo adresářů říká podklíče (pozor, změna terminologie oproti *.INI). Podklíče jsou sdružené do čtyř hlavních skupin, zvaných klíče: • HKEY_CLASSES_ROOT • HKEY_CURRENT_USER • HKEY_LOCAL_MACHINE • HKEY_USERS Každý hlavní klíč leží na vrcholu víceúrovňového stromu podklíčů (subkeys), ekvivalentů podadresářů. Každý podklíč může vlastnit jednu skupinu položek, přičemž jednotlivé položky skupiny mají názvy unikátní uvnitř své skupiny a přiřazenou hodnotu. Každý podklíč může tedy obsahovat několik dalších podklíčů a nejvýše jednu skupinu položek. Chceme-li s obsahem podklíče pracovat - přidávat do něho další podklíče nebo nové položky do jeho skupiny, respektivě měnit hodnoty přiřazené již definovaným položkám, musíme napřed podklíč otevřít. Teprve potom lze provést žádané operace a po jejich skončení se musí podklíč opět uzavřít. Ve struktuře podklíčů se nedá pohybovat jako v adresářích, tzn. přesměrovat aktuální klíč do nižšího, resp. nadřízeného adresáře. Podobné přesuny vyžadují zavření starého podklíče a otevření nového. Služby pro práci se systémovým registrem začínají Reg... a některé z nich mají dvě podoby, lišící se příponou ...Ex. Například RegOpenKey a RegOpenKeyEx. Obě služby vykonávají stejnou činnost - otevření podklíče, ale doporučuje se, existuje-li tento výběr, použít službu zakončenou Ex kvůli kompatibilitě s budoucími verzemi Windows. Jako příklad práce se systémovým registrem si ukážeme zjištění systému souborů. Aplikace mohou ve Windows běžet totiž ve dvou módech - ve Win16 nepodporujícím dlouhá jména a ve Win32 s dlouhými jmény. Potřebujeme-li se to zjistit, z důvodu kompatibility se staršími instalacemi, lze analyzovat hodnotu "Win31FileSystem" umístěnou v registru pod klíčem: "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\FileSystem" Je-li hodnota uvedené položky 0, pak aplikace byla spuštěná v módu s dlouhými jmény, v opačném případě pracuje v módu kompatibilním s Win16: "&
Systémový registr si lze prohlédnou programem regedit.exe, který je součástí OS.
181
HKEY key; BYTE data; DWORD type; DWORD size = sizeof(data);
// // // //
handle otevřeného podklíče systémového registru položka pro uložení dat načtených z registru položka pro uložení typu dat načtených z registru proměnná obsahující velikost paměti rezervované pro data
if ( RegOpenKeyEx( // Otevři systémový registr HKEY_LOCAL_MACHINE, // Hlavní klíč obsahující zadanou informaci "System\\CurrentControlSet\\Control\\FileSystem", // Určení odpovídajícího podklíče NULL, // Rezervováno pro rozšíření OS a musí být NULL. KEY_READ, // Požadovaný přístup k položce &key // adresa proměnné pro handle registru ) == ERROR_SUCCESS // otevření se zdařilo ) { if ( RegQueryValueEx( // Dotaz na hodnotu key, // handle otevřeného registru "Win31FileSystem", // žádaná položka otevřené sekce NULL, // Rezervováno pro rozšíření OS a musí být NULL. &type, &data, // adresy proměnných pro uložení typu dat a jejich hodnoty &size // adresa proměnné obsahující velikost rezervovanou pro data ) == ERROR_SUCCESS // čtení se podařilo ) { if(type == REG_BINARY) // Je binární typ? Tento typ vlastní většina položek { if(data == 0) printf("Dlouhá jména.\n"); else printf("MS-DOS 8.3 jména.\n"); } } RegCloseKey(key); // Uzavření otevřeného systémového registru } Podobný test není nezbytně nutný a slouží pouze pro detekci špatně nakonfigurovaných systémů, chceme-li program obrnit vůči všem nástrahám. U 32 bitové aplikace lze obvykle předpokládat její spuštění v módu s dlouhými jmény. Poznámka: Programy napsané pro Windows 3.1 mohou rovněž používat dlouhá jména, ale za několika předpokladů. Třeba je znovu přeložit, aby se v nich rezervovaly větší proměnné pro jména souborů, a dále se musejí opravit jejich hlavičky tak, aby specifikovaly operační systém Windows 4.0. Toho se dosáhne úpravou výsledného EXE souboru, což vyžaduje znalce podobných zásahů. Programy realizující tyto úpravy lze občas najít na síti - např. Mark95. Po těchto změnách Windows 95 spustí aplikaci v paměťovém mód Win16, ale umožňujícím dlouhá jména.
8.4 Threads- vlákna Vlákno, thread, znamená řetězec prováděných instrukcí. Každý proces vlastní nejméně jedno vlákno. Má-li jich v nějakém okamžiku víc, multithread process, pak se vůči OS stále tváří jako jediný celistvý proces. Přepínání mezi vlákny se provádí snáze než mezi procesy a stará se o něj speciální systémová knihovna bez účasti jádra OS. Vlákna umožňují paralelní programování. Na víceprocesorovém OS teoreticky zrychlují výpočet, avšak pro většinu úloh se dosud nezná efektivní rozklad algoritmu na paralelní operace, takže výsledné zrychlení není úměrné počtu procesorů. Na jednoprocesorovém systému pracuje v každém okamžiku nejvýše jedno vlákno a vícevláknový běh nezrychlí výpočet, spíš naopak, přepínání vyžaduje přídavné operace. Nicméně rozklad řešení na souběžně pracující vlákna pomůže při programování komunikačních úloh nebo obsluh periférií - čekání na vstup a výstup lze využít k jiným operacím. Vlákna dovolují také provádět zdlouhavé operace na pozadí, jako překreslení celého grafu a obdobné úkony. Každé vlákno má svůj vlastní zásobník, avšak všechna navzájem sdílejí virtuální adresový prostor svého procesu, tj. hlavního vlákna. Vytvoří-li se vlákno v GUI aplikaci, přidělí se mu 182
jeho vlastní fronta zpráv a potřebuje-li zpracovávat frontové zprávy, musí si pro ně vytvořit vlastní smyčku. 32 bitový proces 1
Klávesnice
Myš
Hlavní systémová fronta
Systémová fronta 1
Vlákno 1
Systémová fronta 2
Vlákno 2
32 bitový proces 2 Systémová fronta 3
Vlákno 3
Windows
16-ti bitové procesy 16-ti bitová aplikace Systémová fronta 16-ti bitová aplikace
Obr. 8-1 Vlákna, procesy a zprávy Vytvoření dalšího vlákna není složité. Stačí napsat funkci pro vstupní bod vlákna, ekvivalent funkce main či WinMain, a zavolat službu CreateThread. Vstupní bod vlákna má pevně určené typy parametrů (připomínám, že WINAPI, stejně jako CALLBACK, označuje callback funkci): DWORD WINAPI MojeVlakno(DWORD * lpdwParam); Vstupní bod vlákna se uvádí jako parametr. Kvůli tomu existuje v include souboru windows.h také typ pointru na tuto funkci LPTHREAD_START_ROUTINE, deklarovaný jako: typedef DWORD (WINAPI * PTHREAD_START_ROUTINE) (LPVOID lpParameter); typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE; Vlákno dostává jediný parametr lpdwParam, který se mu předává při jeho spuštění, a ostatní data si vyměňuje přes společný adresový prostor s procesem. Funkce CreateThread vytvoří vlákno a vrací na něj handle, potřebný ke všem manipulacím s vláknem. Důležité parametry v CreateThread jsou vyznačené tučně - vstupní bod vlákna a předaný argument. Nezvýrazněné parametry mají význam pro speciální použití. HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // bezpečnostní atributy DWORD dwStackSize, // počáteční velikost zásobníku LPTHREAD_START_ROUTINE lpStartAddress, // startovací funkce vlákna LPVOID lpParameter, // předaný argument vláknu DWORD dwCreationFlags, // Flag vytvoření LPDWORD lpThreadId // adresa proměnné pro uložení identifikátoru vlákna ); Pointer na bezpečnostní atributy lpThreadAttributes se může rovnat NULL, pak místo použijí výchozí atributy. Velikost zásobníku dwStackSize lze nastavit na 0, v tom případě se mu přidělí stejná délka jako hlavnímu procesu (tj. zpravidla megabyte, ale samozřejmě virtuální paměti). Flag vytvoření dwCreationFlags nabývá dvou hodnot, buď je 0, potom je vlákno spuštěno okamžitě po vytvoření, anebo má hodnotu CREATE_SUSPENDED a v tom případě se vlákno sice vytvoří, ale zůstane neaktivní a rozběhne se až po zavolání služby ResumeThread. 183
Poslední parametr lpThreadId udává adresu proměnné, do níž se uloží 32 bitové číslo identifikátoru vlákna (Jeho význam je však dost nejasný - veškeré manipulace s vláknem se provádějí přes jeho handle, avšak lpThreadId se musí zadat a nelze pro něj použít NULL). Vytvořené vlákno je možné pozastavit pomocí SuspendThread, znovu ho spustit ResumeThread, nebo ho ukončit TerminateThread, pokud to v něm prováděné operace dovolují. Dále lze vláknu přidělit prioritu službou SetThreadPriority. Rovněž může samo vyčekat předem určenou dobu, zavolá-li Sleep, resp. SleepEx. Jako příklad si ukážeme Console aplikaci s více vlákny. jelikož ovládáme již objekty, vytvoříme si pro spouštění vláken vlastní objekt, který pak použijeme. #include <windows.h> #include <stdio.h> #include class Vlakno { HANDLE hThread; DWORD dwThreadId; DWORD dwThrdParam;
// handle vytvořeného vlákna // identifikátor vytvořeného vlákna // parametr předaný vláknu
public: Vlakno( LPTHREAD_START_ROUTINE ThreadFunc, DWORD Param ) { dwThrdParam=Param; // Uložíme předaný parametr hThread = CreateThread( // vytvoř vlákno NULL, // výchozí bezpečnostní atributy 0, // vytvoř zásobník stejné délky jako má proces ThreadFunc, // počáteční bod vlákna & dwThrdParam, // argument předaný vláknu 0, // spustit okamžitě po vytvoření & dwThreadId ); // identifikátor vlákna } ~Vlakno() { Terminate(0); CloseHandle(hThread); }
// Ukonči vlákno (dvojí ukončení nevadí ) // Zruš vytvořené vlákno
DWORD Suspend() { return ::SuspendThread(hThread); } DWORD Resume() { return ::ResumeThread(hThread); }
// Pozastav vlákno // Pokračování vlákna
void Terminate(DWORD dwExitCode) { ::TerminateThread(hThread, dwExitCode);} // Konec BOOL Priority(int nPriority) { return ::SetThreadPriority(hThread,nPriority);} // Nastav prioritu };
// Konec deklarace objektu Vlakno
DWORD WINAPI MojeVlaknoA(DWORD * lpdwParam) { DWORD count=0; char cid = (char) (* lpdwParam); // Ulož značku přidělenou vláknu while(count<750000) if(!(++count%10000)) printf(" <%c>", cid ); // Tiskni značku každý 10000 průchod return 0; }
184
DWORD WINAPI MojeVlaknoB(DWORD * lpdwParam) { DWORD count=0; char cid = (char) (* lpdwParam); // Ulož značku přidělenou vláknu while(count<2000000) if(!(++count%10000)) printf(" {%c}", cid ); // Tiskni značku každý 10000 průchod return 0; } void main(void) { Vlakno par1((LPTHREAD_START_ROUTINE) MojeVlaknoA, '1'); Vlakno par2((LPTHREAD_START_ROUTINE) MojeVlaknoA, '2'); Vlakno par3((LPTHREAD_START_ROUTINE) MojeVlaknoB, '3');
// Vytvoř vlákna
DWORD count; for(count=0; count<1500000u; count++) { if( !(count % 10000u) ) // Tiskni značku hlavního vlákna printf(" [0]"); if( count == 200000u ) // Pozastav vlákno 1 { printf("\n1-suspend\n", count); par1.Suspend(); } if( count == 400000u ) // Povol vláknu 1 pokračovat v práci { printf("\n1-resume\n", count); par1.Resume(); } if( count == 600000u ) // Zvyš prioritu vláknu 3 { printf("\n3-priority high\n", count); par3.Priority(THREAD_PRIORITY_ABOVE_NORMAL); } if( count == 800000u ) // Ukonči běh vlákna 1 { printf("\n1-terminate\n", count); par1.Terminate(1); } } while(!kbhit()); }
// Počkat na stisk klávesy před zavřením Console okna
Startovací body vláken MojeVlaknoA a MojeVlaknoB obsahují cykly, které při každém 10000 průchodu tisknou zadaný identifikátor. V hlavním programu se vytvářejí tři vlákna jako objekty typu Vlakno. Funkce MojeVlaknoA pracuje jako dvě vlákna, s identifikátory 1 a 2, což je přípustné vzhledem k tomu, že MojeVlaknoA pracuje pouze s lokálními proměnnými a každé vlákno dostává svůj vlastní zásobník. MojeVlaknoB běží jednou s identifikátorem 3. Základní program, hlavní vlákno procesu, v cyklu tiskne svůj identifikátor [0] každý 10000 průchod a ovládá druhá vlákna. Vlákno 1 pozastaví voláním Suspend, opět ho spustí Resume, posléze vláknu 3 zvýší prioritu a nakonec ukončí vlákno 1 pomocí Terminate. Výsledkem programu bude výpis udávající aktivitu jednotlivých vláken, jeho charakter bude záviset na rychlosti procesoru poèítaèe a momentálním zatí ení OS. Je patrné, že procesor přiděluje úseky časy jednotlivým vláknům podle své úvahy a ty se nemusejí nutně periodicky střídat. Závisí to na prioritách jejich i jiných procesů běžících v pozadí.
185
[0] [0] [0] [0] 1-suspend [0] <2> <2> <2> [0] <2> <2> <2> [0] [0] [0] <2> {3} {3} {3} [0] 1-resume [0] [0] [0] [0] <1> <1> [0] [0] <2> <2> <2> {3} 3-priority high <1> <1> <2> {3} {3} {3} {3} {3} {3} {3} {3} {3} {3} {3} {3} {3} [0] [0] <1> <2> <1> [0] <1> <1> <1> [0] [0] [0] 1-terminate [0] [0] [0] <2> <2> <2> <2> <2> [0] [0] [0] [0]
<1> <1> <1> <2> <2> <2> {3} {3} {3} {3} [0] [0] [0] [0] <2> {3} <2> [0]
<2> {3} <2> [0]
<2> [0] {3} <2>
<2> [0] {3} <2>
<2> <2> [0] <2>
{3} <2> [0] {3}
{3} {3} [0] {3}
{3} {3} [0] {3}
{3} {3} <2> {3}
[0] {3} <2> [0]
[0] <2> <2> [0]
[0] <2> <2> <2>
[0] [0] [0] [0] <2> [0] [0] [0] <2> <2> <2> {3} <2>
[0] [0] {3} {3} {3} {3} <2> <2> <2> <2> <2> <2> <2> <1> <1> <1> [0] [0] [0] [0] [0] [0] [0] [0] [0] <1> <1> {3} {3} <2> <2> <2> {3} {3} {3} {3} [0] [0] [0] {3} {3} [0] {3} <2> <1> [0]
{3} {3} [0] {3} <2> <1>
{3} {3} <1> {3} <2> <1>
{3} {3} <2> {3} <2> <1>
{3} {3} {3} {3} <2> <1>
{3} {3} {3} {3} [0] <1>
{3} {3} {3} {3} [0] <1>
{3} {3} {3} {3} {3} {3} {3} {3} [0] [0] <1> [0]
{3} {3} {3} {3} [0] [0]
{3} {3} {3} {3} [0] [0]
{3} {3} {3} {3} <1> <1>
{3} {3} {3} {3} {3} {3} {3} {3} <1> <1> <1> <1>
{3} {3} {3} {3} {3} {3} {3} {3} <1> <1> <1> <1>
<2> [0] [0] <2> <2> <2> <2> <2> [0] [0] [0] [0] [0] [0] <2> <2> <2> <2> <2> [0] [0] [0] <2> <2> <2> <2> <2> <2> <2> <2> <2> [0] [0] [0] [0] [0] [0] [0] [0] [0] Tabulka 2 - Výstup vícevláknového programu Konec vlákna 3
{3} - Vlákno Priority [0] - Hlavní vlákno procesu Suspend
Resume
Konec procesu Terminate Konec vlákna 1
<1> - Vlákno
Konec vlákna 2
<2> - Vlákno Obr. 8-2 Diagram bìhu vláken
Poznámka1: Násilné ukončení běhu vlákna není doporučovaná operace. V našem příkladu ji lze použít, protože provádíme čistě výpočetní operace, ale pokud by vlákno pracovalo se systémovými zdroji, třeba otevíralo soubory a podobně, pak by použití TerminateThread by mohlo vést ke zhroucení procesu. Poznámka2: Nejsložitější operace s vlákny představují záležitosti kolem jejich synchronizace. Jedná se o zajímavé problémy, avšak ležící mimo hlavní cíle a náplně skript. Studenti katedry řídící techniky se o nich mohou dozvědět více třeba v předmětu "Operační systémy a jejich aplikace".
186
8.5 Zpracování výjimek Zpracování výjimek, exception handling, znamená reakci na neobvyklou událost v programu, která se charakterem vymyká běžné práci. Výjimky se dělí na tři skupiny - na C výjimky, na C++ výjimky a na strukturované výjimky. První dvě skupiny se liší od sebe pouze jinými klíčovými slovy. Vzhledem k předpokladu používání objektového jazyka probereme jen C++ výjimky. Poslední, strukturované výjimky se pak vztahují přímo k činnosti procesoru a ty nás budou zajímat nejvíce.
8.5a
C++ výjimky
Pøevod èísla double na typ WORD (unsigned short int), třeba pro výstup na D/A převodník, by mohl vypadat takto (připomínám, že v C++ se WORD(a+0.5) rovná (WORD) (a+0.5) a při převodu na celé číslo se usekávají desetinná místa): WORD d2w(double a) { return WORD(a+0.5); } // převeď double na WORD se zaokrouhlením Konverzní funkci lze používat za předpokladu, že vstupní hodnota bude zobrazitelná na WORD. Přidá-li se test na přetečení, bude nutné zvolit reakci při překročení povoleného rozsahu. Nejjednodušším řešením bývá omezení výstupu na minimum a maximum: WORD d2w(double a) { if(a<0) return 0; else if(a > 0xFFFFu) return(0xFFFFu); else return WORD(a+0.5); } Limitace pohřbila informaci, že převáděná hodnota ležela mimo rozsah. Bude-li se pomocí d2w například rozkládat vektor rychlosti na jeho složky x a y: DAosaX = d2w(rychlost * cos(uhel)); DAosaY = d2w(rychlost * sin(uhel)); pak D/A převodníky DAosaX a DAosaY sice nepřetečou, ale omezením záporných čísel v d2w na 0, dojde nesprávnému rozkladu vektoru. Vhodnější by bylo limitaci detekovat, aby se dala provést vhodná korekce - třeba přepnutí směru točení motorů. To zařídí další testy anebo výjimka. Výjimku lze pokládat za jakýsi odskok při výskytu mimořádné situace: WORD d2w(double a) throw(const char *) { if(a<0 || a > 0xFFFFu) throw("Vektror rychlosti mimo kvadrant."); else return WORD(a+0.5); } Klíčové slovo throw (metat, házet výjimku) se vyskytuje jednak v hlavičce funkce a jednak v místě, kde se výjimka metá, čili provádí odskok. Parametrem throw může být libovolný objekt nesoucí informaci o výjimce. Tělo funkce smí obsahovat několik různých příkazů throw s různými objekty, avšak seznam jejich typů musí uvedený v throw hlavičce funkce void fce(int i) throw(char, double, const char *) { if(i==0) throw('0'); if(i==1) throw('1'); if(i==2) throw(double(2)); if(i==3) throw("cislo 3"); } Chytání výjimek se provádí v bloku tvořeném dvojicí klíčových slov try (pokusit se) a catch (chytit) a pro uvedený příklad by vypadalo třeba takto: try // Začátek bloku výjimek - pokus se provést následující operace { DAosaX = d2w(rychlost * cos(uhel)); DAosaY = d2w(rychlost * sin(uhel)); } catch(const char * str) // Chytání výjimek, které metají objekt typu const char { printf("%s\n",str); /* Analýza chyby, přepnutí směru točení motoru, nový výpočet. */ } 187
Výjimky mají oproti klasickým testů výhodu, že pracují i po vnoření operací do jiné funkce. void NastavDA(double rychlost, double uhel) { DAosaX = d2w(rychlost * cos(uhel)); DAosaY = d2w(rychlost * sin(uhel)); } Funkce NastavDA zachová výjimku funkce d2w, i když nemá throw uvedené v hlavičce (viz. dále poznámka na konci této kapitoly): try { NastavDA(rychlost, uhel); } catch(const char * str) { printf("%s\n",str); /*.... */ } Mechanismus ošetření výjimek pracuje na principu vytváření pomocného seznamu, jehož adresa je učena registry procesoru (zpravidla segment registrem FS jako FS:0). Při vstupu do bloku výjimek či do funkce metající výjimku, tj. funkce s throw v hlavičce, se k seznamu připojí další záznam. Ten se opět zruší po ukončení dané části. Seznam tak obsahuje zřetězené informace o blocích a funkcích výjimek, do nichž je vlákno právě vnořené. Vyskytne-li se výjimka, knihovní funkce obsluhující C++ výjimky vyhledá v seznamu vhodné catch chytající vržený objekt. Najde-li ho, zruší adekvátní část zásobníku, jako by došlo k příkazům return, čili vynoření z funkce či z mnoha funkcí až na příkazový blok catch. Na ten se pak předá řízení. V opačném případě, není-li nalezeno vhodné catch, provede se default obsluha výjimky, obvykle ukončení programu. Výjimky dovolují použít i vícenásobné příkazy catch a dále několik vnořených bloků try v sobě. Má-li try chytit veškeré výjimky, může se mu jako argument zadat operátor ellipsis, tři tečky za sebou bez vložených mezer ... (ellipsis = česky výpustka neboli elipsa ), který v C označuje libovolný parametr. Ukažme to na programu pro výpočet odstředivého zrychlení: #include <stdio.h> #include typedef unsigned short int WORD;
// typ WORD
int d2w(double a) throw(const char *) // převod na WORD metající výjimku const char * { if(a>0xFFFF || a<0) throw("Data mimo rozsah"); else return WORD(a+0.5); } double Aodstr(double v, double r) throw(char) // výpočet metající výjimkou char { if(r<=1e-10) throw( char('0') ); else return (v*v)/r; } int IAodstr(double v, double r) { return ( d2w(Aodstr(v,r)) ); } void main(void) { try { printf( "%d\n",IAodstr(100,1) ); try { printf( "%d\n",IAodstr(1000,1) ); printf( "%d",IAodstr(100,0) ); } catch(char c) { printf("Výjimka B2: %c\n", c); }
// 1. blok try - catch // 2. blok try - catch // → Data mimo rozsah //→ → Výjimka B2: 0
printf( "%d",IAodstr(1000,0) ); // → Výjimka 0 throw(1); // → Neznámá výjimka } catch(const char *str) { printf("%s\n",str); } catch(char c) { printf("Výjimka %c\n",c); } // chytej char objekt odlišného typu catch(...) { printf("Neznámá výjimka\n"); } // chytej vše - smí být jen jako poslední printf("Stiskněte klávesu."); while( !kbhit() ); }
188
Poznámka 1: Program samozřejmě skončí na první výjimce, v našem případě "Data mimo rozsah". Ta se musí odstranit ze zdrojového kódu, mají-li se ukázat další výjimka. Poznámka 2: Pokud funkce sama metá nějakou výjimku, potom její hlavička musí obsahovat všechny typy objektů výjimek, které generuje buď ona nebo v ní použité funkce. Chybou by byl následující zápis: double IAodstr1(double v, double r) throw(char) { if(r<=1e-10) throw( char('0') ); else return d2w(v*v)/r; } protože funkce d2w metá výjimku const char *, která není deklarovaná v hlavičce IAodstr1. Správný kód je proto: double IAodstr1(double v, double r) throw(char, const char *) { if(r<=1e-10) throw( char('0') ); else return d2w(v*v)/r; } Na funkce, které samy nemetají výjimku, tj. neobsahují ve svém těle throw, se pravidlo nevztahuje. I když nemají ve své hlavičce throw, pak výjimky všech v nich užitých funkcí se dostanou z nich ven. Viz. funkce NastavDA z této kapitoly. Poznámka 3: Visual C++ (5.0) nerealizuje pro výjimky úplnou normu ANSI C++. Kromě jiného nedovoluje metat všechny objekty a za throw vyžaduje return.
8.5b
Strukturované výjimky
Přidáme-li do předchozího programu následující kód: try { double d=0; d=5/d; // → Výjimka 10H v modulu WIN32.EXE int * pi = (int *) -1; *pi = 0; // → Aplikace způsobila neplatnost stránky } catch(...) { printf("Výjimka\n"); } dočkáme se nepříjemného překvapení - každá operace ukončí aplikaci s chybou bez ohledu na ošetření výjimky v bloku try - catch. Dělení nulou a adresace mimo rozsah vyvolávají přerušení na úrovni procesoru, na něž OS reaguje strukturovanou výjimkou (structured exception). Ta se sice obhospodařuje podobným mechanismem jako C++ výjimka, ale na úrovni OS, a proto pro rozlišení vyžaduje jinou deklaraci. Blok strukturovaných výjimek se podobná C++ výjimkám, začíná také klíčovým slovem try, ale v catch se místo chytaného objektu používá filtr výjimek. try // Pozn.: V neobjektovém jazyce C se místo try píše __try { /* try block */ } __except ( filter-expression ) // Filtr výjimek. Pozn.: V neobjektovém jazyce C: __finally { /* exception handler = příkazový blok ošetření výjimek */ } Přechod na příkazový blok __except provádí sám OS, který hledá první splněnou podmínku podle následující postupu: 1. Je-li aktivní debugger, předá mu řízení. 2. Hledá se exception handler v seznamu bloků výjimek a je-li, provede se sekce __except. 3. Vykoná druhý pokus lokalizovat debugger. 4. Selže-li vše předchozí, provede náhradní exception handler, obvykle ExitProcess . Filter pøedstavuje novinku obsluhy strukturovaných. Je jím libovolná u ivatelská funkce, která splòuje pøedpoklad, e vrací jednu ze dvou hodnot: EXCEPTION_EXECUTE_HANDLER - výjimka byla obsloužená EXCEPTION_CONTINUE_EXECUTION - výjimka nebyla obsloužená
189
Filter mù e mít libovolné argumenty. Obvykle se mu pøedává kód výjimky, získaný jednou ze dvou mo ných slu eb OS: DWORD GetExceptionCode( VOID ); // Vrať pouze kód výjimky LPEXCEPTION_POINTERS GetExceptionInformation( VOID ); // Získej kód, obsah registrů,... Tyto služby se musejí bezpodmínečně volat výhradně uvnitř bloku __except( /* tj. zde*/ ), protože mimo něj nedávají správné hodnoty. GetExceptionInformation vrací nejen specifikaci výjimky, ale i podrobné údaje o obsahu registrech procesoru, obsahu zásobníku a podobně. Všechny tyto hodnoty vypisuje OS při pádu aplikace. GetExceptionCode vrací pouze číslo strukturované výjimky, k níž došlo. Nejčastější bývají tyto: EXCEPTION_ACCESS_VIOLATION - narušení ochrany paměti EXCEPTION_FLT_DIVIDE_BY_ZERO, - reálné dělení 0 EXCEPTION_FLT_INVALID_OPERATION EXCEPTION_INT_DIVIDE_BY_ZERO EXCEPTION_PRIV_INSTRUCTION
- nedovolená reálná operace, např. − 1 - celočíselné dělení 0 - vykonaná chráněná instrukce, jako třeba změna hodnoty segment registru
EXCEPTION_NONCONTINUABLE_EXCEPTION - pokus pokračovat po uživatelské výjimce, která to
zakazuje, viz. dále. Strukturované výjimky mù e metat i u ivatelský program funkcí: VOID RaiseException( // Metej strukturovanou výjimku DWORD dwExceptionCode, // náš vlastní kód výjimky DWORD dwExceptionFlags, // 0 nebo EXCEPTION_NONCONTINUABLE DWORD cArguments, // délka pole lpArguments, může být 0 CONST DWORD * lpArguments // argumenty, může být NULL ); Pole lpArguments určuje libovolnou informaci a jeho adresa se předává filtru výjimek a lze ho přečíst GetExceptionInformation. Počet prvků DWORD pole lpArguments nesmí být větší než konstanta EXCEPTION_MAXIMUM_PARAMETERS. Ošetření strukturovaných výjimek nejlépe ukáže příklad: #include <windows.h> #include <stdio.h> #include int TestException(unsigned long syskod) // filtr výjimek { switch(syskod) // větvení podle čísla výjimky { case EXCEPTION_ACCESS_VIOLATION: printf("Narušeni paměti"); break; case EXCEPTION_FLT_DIVIDE_BY_ZERO: printf("Děleni 0"); break; case 0xFFFF: printf("Můj kód 0xFFFF"); break; default: return EXCEPTION_CONTINUE_SEARCH; // Nenalezeno, pokračuj v hledání. } return EXCEPTION_EXECUTE_HANDLER; // Nalezeno a obslouženo, stop hledání. }
190
void main(void) { DWORD exceptioncode; try { double d =0; d=5/d; // Výjimka: Reálné dělení 0 int * pi = (int *) -1; *pi = 0; /* Narušení ochrany paměti. Má-li program dojít až sem, musí se z kódu samozřejmě vyřadit řádek generující předchozí výjimku.*/ RaiseException( 0xFFFF, 0, 0, NULL ); // Generujeme naši výjimku 0xFFFF } __except( TestException( exceptioncode = GetExceptionCode()) ) // Filtr { printf(" - výjimka WIN32\n"); if( exceptioncode==0xFFFF ) printf("Náš konec!\n"); } printf("Stiskněte klávesu."); while( !kbhit() ); } Oba druhy výjimek (C++ a strukturované) lze kombinovat: /*...........*/ try // C++ výjimka, měla by být vnějším blokem { try // Strukturované výjimky - vnitřní blok { printf("%d\n",IAodstr(1000,1)); // Operace programu d=0; d=5/d; } __except( TestException( exceptioncode = GetExceptionCode()) ) { if( exceptioncode==0xFFFF ) printf("Náš konec!\n"); esle printf(" - WIN32\n"); } } catch(const char *str) { printf("%s\n",str); } catch(...) { printf("Neznámá C++ výjimka\n"); } /*...........*/ Poznámka: Strukturované výjimky představují významný prvek Win32, avšak nedůsledně používaný. Většinu havárií programů, při nichž se objeví dialog informující, že aplikace provedla narušení paměti nebo jinou chybu, způsobilo nedostatečné nebo zcela chybějící ošetření výjimek. Laická veřejnost pak tyto situace připisuje mylně chybám v OS, k velké radosti mnohých softwarových firem.
8.6 Virtuální alokace Virtuální adresace se probírala v kapitole 1.3, přičemž se mluvilo o stránkách paměti. Ty obvykle zaujímají 4 kB, což je dané mapovacím systémem procesoru a jejich přesnou velikost na OS, včetně dalších charakteristik lze zjistit voláním služby: VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo ); která vyplní údaje ve struktuře typu SYSTEM_INFO obsahující řadů systémových informací jako rozsah adresového prostoru, typ procesoru, počet procesorů. Její člen dwPageSize pak nese údaj o velikosti adresové stránky. Okamžitý stav paměti řekne další funkce: VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer); vyplňující strukturu MEMORYSTATUS s údaji o volné paměti. Do jejího prvního členu dwLength se musí před použitím zapsat délka struktury, podobně jako do SECURITY_ATTRIBUTES struktury používané CreateFile.
191
Win32 dovolují uživateli využívat mechanismus virtuální alokace pomocí služby OS: LPVOID VirtualAlloc( LPVOID lpvAddress, // určení požadované adresy začátku paměti DWORD cbSize, // požadovaná velikost paměti DWORD fdwAllocationType, // typ alokace DWORD fdwProtect // požadovaná přístupová práva ); Jednotlivé parametry mají následující významy: lpvAddress - pointer ukazující na proměnnou, která obsahuje počáteční adresu požadovaného adresového prostoru. OS požadavek zváží, zaokrouhlí ho na začátek nejbližší volné stránky a do proměnné uloží výslednou adresu. Pokud se místo lpvAddress zadá NULL, pak se umístění ponechává na OS. cbSize - určuje požadovanou velikost. Tu OS upraví na celý počet stránek, tzn. že alokace 1 byte má za následek přidělení celé stránky. fdwAllocationType - typ alokace je logickou kombinací bitových atributů: MEM_TOP_DOWN - alokace se provede na nejvyšší možné adrese. MEM_COMMIT - alokuje stránky ve fyzické paměti nebo v odkládacím prostoru na disku (swap file). MEM_RESERVE - pouze rezervace adresového prostoru bez vyhrazení jakékoliv fyzické paměti. Rezervované stránky paměti nebudou používané jinými operacemi alokace, dokud nedojde k jejich uvolnění a dalšími voláními VirtualAlloc jim lze přidělit fyzickou paměť (MEM_COMMIT). fdwProtect - ochrana alokace; ta nabývá několika hodnot, z nichž se nejčastěji používají: PAGE_READONLY - paměť lze pouze číst, při pokusu a zápis dojde k výjimce. PAGE_READWRITE - zápis i ètení. PAGE_EXECUTE - povolen bìh program, pokus o zápis èi ètení vyvolá výjimku. PAGE_EXECUTE_READ - povolen bìh programu a pokus o zápis vyvolá výjimku. PAGE_EXECUTE_READWRITE - povoleno vše. PAGE_NOACCESS - jakýkoliv dostup na stránku vyvolá výjimku. Pozn. Zadaná ochrana se mù e pozdìji zmìnit voláním VirtualProtect. Funkce VirtualAlloc vrací buï adresu alokované pamìti anebo NULL. Alokované stránky uvolňuje VirtualFree. V prùbìhu práce s VirtualAlloc mohou stránky adresového prostoru nabývat tří stavů: Volné (free) - stránky patřící do adresového prostoru procesu, které nejsou nikomu přidělené. Rezervované (reserved) - vyhrazené stránky adresového prostoru, které nesmějí být přidělené dalším alokacím, avšak ještě nedostupné, protože pro ně neexistuje mapování. Rezervovaný prostor lze uvolnit VirtualFree, tj. převést na volný a dovolit jeho používání, anebo k němu přidělit paměť dalším voláním VirtualAlloc. Přidělené (committed) - stránky adresového prostoru mající přidělenou stránkovanou paměť a lze s nimi pracovat v mezích zadané ochrany. Použití virtuální alokace si ukážeme na příkladu řídké matice, mající většinu prvků nulových s výjimkou několika členů. Podobné matice se vyskytují třeba při maticovém vyjádření grafů nebo Petriho sítí. Naše matice bude rozměru [10000,10000] prvků double, což vyžaduje 800 MB paměti. Její velikost se virtuální alokací sníží na několik desítek kilobytů. Toho se dosáhne tak, že se paměť přidělí jenom částem matice, které obsahují nenulové prvky. Kvůli pocvičení v objektech vytvoříme třídu SPAREMATRIX (řídká matice). Její konstruktor alokuje paměť VirtualAlloc a dvě metody Cti a Uloz, které budou na základě strukturovaných výjimek kontrolovat přístup k prvkům. Nebude-li daná stránka existovat, Cti vrátí nulu, zatím192
co ve stejném případě Uloz přidělí paměť voláním VirtualAlloc a údaj do ní zapíše. Nutnost ošetřovat výjimky nedovoluje napsat operátor [], protože ten by musel vracet referenci na objekt, což je sice možné, ale znamenalo by to ošetřit výjimky vně objektu. Příklad rozdělíme do tří souborů: • sparem.h - deklarace třídy SPAREMEM; • sparem.cpp - definice Uloz a Cti; • maim.cpp - program používající třídu řídkých matic. /*========================= SPAREM.H =========================*/ #ifndef _SPAREM_H_ #define _SPAREM_H_ #include <stdio.h> #include <windows.h> class SPAREMATRIX { typedef double MATRIX[10000]; DWORD PAGESIZE; SYSTEM_INFO sysinfo; MATRIX * pdMatrix; int pgcount;
// prevence vícenásobného vložení souboru sparem.h
// // // // //
Pomocný typ pro deklaraci - řádek matice Velikost stránky Systémová struktura pro zjištění velikosti stránky Alokovaná matice počet alokovaných stránek
int PageFault(DWORD dwCode) // Filtr výjimek { if (dwCode == EXCEPTION_ACCESS_VIOLATION) return EXCEPTION_EXECUTE_HANDLER; else return EXCEPTION_CONTINUE_SEARCH; }
// Výjimka narušení paměti // Výjimku -zpracováváme // - nezpracováváme
public: SPAREMATRIX(DWORD size) throw (const char * ); // C++ výjimka při chybě alokace ~SPAREMATRIX() { VirtualFree(pdMatrix, 0, MEM_RELEASE ); } // Uvolni celou paměť void Info() { printf("Užito %d stránek = %d bytů.\n", pgcount, pgcount * PAGESIZE); } void Uloz(int x, int y, double dTemp) throw(const char *); double Cti(int x, int y);
// Zapiš: Matice[x,y] = dTemp; // C++ výjimka při nedostatku paměti // Čti prvek: Matice[x,y]
}; #endif //_SPAREM_H_ /*====================== SPAREM.CPP ===================*/ #include "sparem.h" SPAREMATRIX:: SPAREMATRIX(DWORD size // Konstruktor udává počet prvků double ) throw (const char * ) // C++ výjimka při chybě alokace { GetSystemInfo(&sysinfo); // Zjisti informace o systému PAGESIZE = sysinfo.dwPageSize; // Ulož velikost stránky paměti pgcount=0; // Počet přidělených stránek na začátku = 0 pdMatrix = (MATRIX *) VirtualAlloc ( NULL, // Výběr přidělené adresy nechán na OS size * sizeof(double), // Požadovaná velikost MEM_RESERVE, // Pouze rezervace PAGE_NOACCESS // Do paměti nelze přistupovat ); if(pdMatrix==NULL) throw("Nedostatek paměti"); }
193
void SPAREMATRIX::Uloz(int x, int y, double dTemp) throw(const char *) { LPVOID lpvResult; try { pdMatrix[x][y] = dTemp; } // Pokus o zápis [x,y] = dTemp; __except ( PageFault(GetExceptionCode()) ) // Dojde-li ke strukturované výjimce { if ( dTemp != 0.0 ) // Paměť přidělit pouze nenulovým elementům { lpvResult = VirtualAlloc( (LPVOID) ( &pdMatrix[x][y] ), // Adresa paměti PAGESIZE, MEM_COMMIT, // Přiděl paměť stránce PAGE_READWRITE ); // Přístup pro čtení i zápis if(lpvResult ==NULL) throw("Nedostatek paměti"); // Metej C++ výjimku pgcount++; pdMatrix[x][y] = dTemp; // Zvyš čítač přidělených stránek a zapiš prvek printf("Zapsáno MATRIX[%d,%d] = %f\n", x, y, dTemp); } } } double SPAREMATRIX::Cti(int x, int y) // Čti prvek: Matice[x,y] { double dTemp; try { dTemp = pdMatrix[x][y]; } // Pokus o zápis __except(PageFault(GetExceptionCode())) // Strukturovaná výjimka { dTemp = 0.0; printf("Výjímka:\t"); } // Vrať 0 return dTemp; } /*==================== MAIN.CPP =========================*/ #include "sparem.h" void main(void) // WIN32 - Console aplikace { int x, y, i; try // Blok C++ výjimek { SPAREMATRIX matrix(10000*10000); // Matice pro 100e+6 čísel typu double for (i = 0; i < 10; i++) // Zapiš náhodně 10 elementů do matice matrix.Uloz( rand() % 10000, rand() % 10000, (double) rand() ); matrix.Info(); do { printf("Zadej řádku, sloupec: "); scanf("%d,%d", &x,&y); // čti indexy prvku [x, y] printf("MATRIX[%d,%d] = %f\n", x, y, matrix.Cti(x,y)); // Výpis uložené hodnoty } while(1); // Opakuj dokud není stisknuto Ctrl-C } catch(const char * str) { printf("C++ výjimka: %s", str); } } Výstup našeho programu by mohl vypadat třeba takto: Zapsáno MATRIX[982,130] = 346.000000 Zapsáno MATRIX[7117,1656] = 1090.000000 Zapsáno MATRIX[2948,6415] = 17595.000000 Zapsáno MATRIX[4558,9004] = 31126.000000 Zapsáno MATRIX[8492,2879] = 3571.000000 Zapsáno MATRIX[6721,5412] = 1360.000000 Zapsáno MATRIX[7119,5047] = 22463.000000 Zapsáno MATRIX[3985,7190] = 31441.000000 Zapsáno MATRIX[252,7509] = 31214.000000 Zapsáno MATRIX[9816,4779] = 26571.000000 Užito 10 stránek = 40960 bytů. Zadej řádku, sloupec: 2000,3000 Výjimka: MATRIX[2000,3000] = 0.000000 Zadej řádku, sloupec: 10000,30000 Výjimka: MATRIX[10000,30000] = 0.000000 Zadej řádku, sloupec: 8492,2879 MATRIX[8492,2879] = 3571.000000
194
Alokace po stránkách není příliš výhodná pro malé bloky, a proto knihovny překladačů obvykle nad ní definují vlastní mechanismus. Ten si od OS bere bloky stránkované paměti, které přiděluje po menších částech, třeba jednotlivým voláním new. Avšak pozor na dynamické alokace malých bloků. Virtuální mapování a 4 GB adresového prostoru nevylučují nebezpečí rozfragmentování paměti. Ukažme si to na alokaci dvou seznamů: #include <windows.h> #include <stdio.h> typedef void * pvoid; // pomocný typ - pointer typu void void main(void) { MEMORYSTATUS ms; // Struktura pro zjištění stavu paměti SYSTEM_INFO sysinfo; // Struktura pro zjištění velikosti stránky void * sez1start = NULL, * sez2start = NULL; // Adresy začátků seznamů pvoid * sez1 = & sez1start, * sez2 = & sez2start; // Aktuální adresy konců seznamu pvoid * sez1new, * sez2new; // Pomocné prvky pro nové členy seznamu int aloc2size; // Velikost prvku druhého seznamu int maxcounter, counter=0; // Maximální a aktuální počet prvků seznamu GetSystemInfo(&sysinfo); printf("Velikost stranky = %ld bytu.\n", sysinfo.dwPageSize); ms.dwLength=sizeof(ms); GlobalMemoryStatus(&ms); printf("Volna fyzicka pamet = %ld bytu.\n", ms.dwAvailPhys); maxcounter = ms.dwAvailPhys/ sysinfo.dwPageSize; // Zaplníme celou fyzickou paměť aloc2size = sysinfo.dwPageSize/sizeof(pvoid)-6; // Alokovaná délka 1. a 2. členu = stránka do // 1. a 2. seznam vytváříme řetězením dílčích alokací { sez1new = new pvoid; if(sez1new!=NULL) { * sez1new = sez1; sez1 = sez1new; } sez2new = new pvoid[aloc2size]; if(sez2new!=NULL) { * sez2new = sez2; sez2 = sez2new; } counter++; } while(sez1!=NULL && sez2!=NULL && counter < maxcounter); printf("Pridelena pamet = %ld bytu.\n", counter* sysinfo.dwPageSize); // Uvolníme všechny prvky druhého seznamu while(sez2!=NULL) { sez2new =(pvoid *)(* sez2); delete[] sez2; sez2=sez2new; } printf("Nyni zabiram pamet = %d bytu. Opravdu?\n",counter*sizeof(pvoid)); ms.dwLength=sizeof(ms); GlobalMemoryStatus(&ms); printf("Volna pamet = %ld bytu.\n", ms.dwAvailPhys); // Uvolníme prvky druhého seznamu while(sez1!=NULL) { sez1new = (pvoid *)(* sez1); delete[] sez1; sez1=sez1new; } Sleep(5000); // Pauza 5 vteřin před zavřením Console okna } sez2start sez1start 1
2 Stránka n1
1
2 Stránka n2
1
2 Stránka n3
Obr. 8-3 Alokace dvou seznamů
195
1
2 Stránka n4
sez2 sez1
Výpis programu bude vypadat takto (počítač měl 32 MB paměti): Velikost stranky = 4096 bytu. Volna fyzicka pamet = 2551808 bytu. Pridelena pamet = 2551808 bytu. Nyni zabiram pamet = 2492 bytu. Opravdu? Volna pamet = 0 bytu.
Několik komentářů k příkladu: • Výpočet aloc2size, délky prvku 2. seznamu, vychází z toho, že každá alokace potřebuje paměť pro element a plus něco navíc pro interní mechanismus správy paměti prováděný new - obvykle kolem 10 bytů, což znamená, že alokace 4 bytů (tj. pointer void *) zabere 14 bytů paměti. • Člen dwAvailPhys struktury MEMORYSTATUS udává volnou RAM paměť, která je okamžitě k dispozici bez odkládání na disk. Windows ji udržují poměrně malou i na počítačích s velkou pamětí. Aplikace, které chtějí hodně prostoru, musejí postupovat agresivněji a řídit se členem dwTotalPhys, který udává celkovou velikost RAM paměti. Tu však není dobré alokovat celou, ale maximálně tak 50% až 75% její hodnoty, chceme-li se vyhnout periodickému odkládání paměti na disk. (Windows potřebují spoustu interních tabulek.) • Program alokuje střídavě dva seznamy, přičemž velikosti prvků v prvním a v druhém seznamu jsou zvolené tak, aby zaplnily celou stránku. Ta obsahuje jeden prvek z každého seznamu. Zrušením seznamu 2 se proto neuvolní paměť na úrovni OS. • Výsledný jev se nazývá fragmentací - drobné alokace blokují adresový prostor. Alokace operátorem new by sice mohla na zablokovaných stránkách přidělovat paměť, ale pouze malé úseky. Při požadavku na větší paměť, by OS musel uvolnit místo odložením stránek paměti na disk (do swap souboru). Samozřejmě i nový prostor lze popsaným způsobem beznadějně rozfragmentovat, a tak dále až do úplného vyčerpání místa na disku. • Způsob alokace v příkladu je sice umělý, ale nikoliv vyloučený. Operace se ve Windows provádějí na střídačku a nemusí se přitom jednat o vlákna, stačí obsluha zpráv. Dávejte proto pozor na alokace malých bloků s dlouhým duration.
8.7 Mapování souborů a sdílení paměti Procesy občas potřebují rychlý přístup k souborům. Win32 nabízejí mapování souborů, které zobrazí jejich obsah do adresového prostoru. Služby CreateFileMapping a MapViewOfFile, které to dovolují, současně umožňuje sdílet paměť mezi procesy, což se používá pro rychlý přenos dat, zejména v databázově orientovaných systémech. Mapování je pro Win32 jediný povolený mechanismus. Metody Win16, výměna dat přes dynamické knihovny a globální alokace sdílené paměti, již neexistují. Mapování vytvoří služba CreateFileMapping: HANDLE CreateFileMapping( HANDLE hFile, // handle souboru LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // bezpečnostní atributy DWORD flProtect, // ochrana mapování DWORD dwMaximumSizeHigh, // horních 32 bitů velikosti objektu DWORD dwMaximumSizeLow, // dolních 32 bitů velikosti objektu LPCTSTR lpName // identifikační jméno objektu ); hFile - handle souboru, který musí být otevřený CreateFile ve vhodném přístupovém módu. Doporučuje se, aby tento soubor nepovoloval sdílení (share). Mapování lze vytvořit i ve stránkovém souboru (swap), v tom případě se místo hFile zadá (HANDLE) -1. Konverze typu (HANDLE) je zde nezbytná. 196
lpFileMappingAttributes - bezpečnostní atributy, které mohou být NULL; v tom případě se pro ně použijí výchozí hodnoty. flProtect - udává ochranu pro vstup do souboru a může být jedním s atributů: PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY kombinovaným s dalšími atributy: SEC_COMMIT - přidělí fyzickou paměť, to je výchozí hodnota. SEC_IMAGE - soubor bude obsahovat spustitelný kód. SEC_NOCACHE - nepovoluje se cashe souboru. SEC_RESERVE - rezervuje adresový prostor, ale nepřidělí mu fyzickou paměť. Tu lze získat voláním VirtualAlloc. Tento atribut smí být nastavený výhradně, pokud se použije hFile rovný (HANDLE) 0xFFFFFFFF, tj. swap soubor. dwMaximumSizeHigh, dwMaximumSizeLow - 64 bitové číslo maximální velikosti mapovaného souboru rozdělené na horních a dolních 32 bitů. Lze tedy pracovat až se soubory o délce 36*10 18 bytů. (Snad dostatečná rezerva pro budoucí verze softwaru.) lpName - identifikační jméno mapovaného objektu. Všechny objekty se stejným jménem budou navzájem sdílené, samozřejmě, když jim to dovolí bezpečnostní atributy. Jméno má význam pro sdílení, jinak lze místo něho použít NULL. Funkce vrací pouze handle mapování, který nedovoluje ještě přístup do paměti. K tomu potřebujeme pointer. Ten se získá funkcí MapViewOfFile, která vybranému úseku přidělí adresový prostor. Lze vytvořit víc pointrů do jednoho mapování. LPVOID MapViewOfFile( HANDLE hFileMappingObject, // handle mapování DWORD dwDesiredAccess, // přístupový mód DWORD dwFileOffsetHigh, // horních 32 bitů pozice v souboru DWORD dwFileOffsetLow, // dolních 32 bitů pozice v souboru DWORD dwNumberOfBytesToMap // počet mapovaných bitů ); kde jednotlivé parametry mají význam: hFileMappingObject - handle vrácený CreateFileMapping dwDesiredAccess - požadovaný přístup, který nesmí být v konfliktu s přístupem zadaným při vytváření mapování: FILE_MAP_WRITE nebo FILE_MAP_ALL_ACCESS - dovolují zápis a čtení FILE_MAP_READ - pouze čtení. FILE_MAP_COPY - pouze zápis. dwFileOffsetHigh, dwFileOffsetLow - výchozí pozice v souboru, 64 bitové číslo ve dvou částech dwNumberOfBytesToMap - zvolená délka úseku. Je-li 0, pak se zpřístupní celý soubor. Na ukázku vytvoříme objekty ShareMemory a ViewToSharedMemory. Jejich include soubor shmem.h se bude vkládat do všech programů potřebujících vzájemně sdílet paměť.
197
/*********************************** SHMEM.H *************************************/ #ifndef _SHMEM_H_ // prevence vícenásobného vložení souboru shmem.h #define _SHMEM_H_ #include <windows.h> class SharedMemory // Vytvoř mapování ve swap souboru { HANDLE hmmf; // handle vytvořeného mapování public: SharedMemory(char * jmeno, DWORD delka) // identifikační jméno mapování a délka throw(const char *); // Výjimka pokud nelze mapování vytvořit ~SharedMemory() { if(hmmf!=NULL) CloseHandle(hmmf); } // Zruš vytvořené mapování HANDLE GetHandle() { return hmmf; } };
// Vrať handle na mapování
class ViewToSharedMemory // Přiřaď mapování adresový prostor { void * lpView; // Pointer na přiřazenou paměť public: ViewToSharedMemory(SharedMemory & shm, DWORD pozice=0, DWORD cbMap=0) throw(const char *); // Výjimka, pokud nelze pointer vytvořit ~ViewToSharedMemory() // Destruktor: Zruš pointer do mapování { if( lpView!=NULL ) UnmapViewOfFile(lpView); } char * cAddr() { return (char *) lpView; } char & operator [] (DWORD index) { return *( (char *) lpView + index ); } }; #endif // _SHMEM_H_
// Vrať pointer typu char // Operátor [] pro přístup jako do pole znaků
/*********************************** SHMEM.CPP *************************************/ #include "shmem.h" // Metody s výjimkou se nepřekládají jako inline, a proto se definují vně tříd.y SharedMemory:: SharedMemory( char * jmeno, // identifikační jméno mapování DWORD delka) // maximální délka throw(const char * ) // výjimka pokud nelze mapování vytvořit { hmmf = CreateFileMapping( (HANDLE) -1, NULL, PAGE_READWRITE, 0, delka, jmeno ); if(hmmf==NULL) throw("CreateFileMapping vrátila NULL."); } ViewToSharedMemory::ViewToSharedMemory( // přiděl adresový prostor SharedMemory & shm, // vytvořený objekt pro sdílení paměti DWORD pozice, // počáteční pozice DWORD delka // délka úseku ) throw(const char *) // Výjimka, nelze-li přidělit { lpView = MapViewOfFile(shm.GetHandle(), FILE_MAP_WRITE, 0, pozice, delka ); if(lpView==NULL) throw("MapViewOfFile vrálila NULL."); } Vytvořený objekt lze použít například ve dvojici aplikací - klient a server, přičemž server čte řádky z klávesnice a přes sdílenou paměť je posílá klientovi.
198
/*********************************** MSERVER.CPP *************************************/ #include <windows.h> #include <stdio.h> #include #include "shmem.h" void main(void) { try { SharedMemory Srv("MojePamet",10000); // sdílená paměť "MojePamet" o délce 10000 ViewToSharedMemory Text(Srv); // celou paměť zobraz do adresového prostoru do { gets( & Text[0] ); // načti řádku z klávesnice a ulož ji do sdílené paměti printf( "Do pameti ulozen text: %s\n", & Text[0]); } while( Text[0] != 'X' ); // začíná-li text velkým 'X', pak konec printf("Vstupni text zacina X - Server konci."); } catch(const char * str) { printf("Vyjimka: %s\n"); } Sleep(5000); // Pauza 5 vteřin před zavřením Console okna } /*********************************** MKLIENT.CPP *************************************/ #include <windows.h> #include <stdio.h> #include "shmem.h" void main(void) { try { SharedMemory Klient("MojePamet",10000); // sdílená paměť "MojePamet" o délce 10000 ViewToSharedMemory Text(Klient); // celou paměť do adresového prostoru printf("Cekam na zmenu obsahu sdilene pameti\n"); do { Text[0] = 0; while ( Text[0]==0 ) Sleep(1000); // čekáme na změnu prvního bytu printf( "Zmena ve sdilena pameti: %s\n", & Text[0]); // Vypíšeme změnu } while( Text[0] != 'X' ); // Je-li první byte velké 'X', pak konec printf("Obsah sdilene pameti zacina X - Klient konci."); } catch(const char * str) { printf("Vyjimka: %s\n"); } Sleep(5000); // Pauza 5 vteřin před zavřením Console okna }
199
9 Služby OS a dynamické knihovny Tato kapitola se hlouběji zabývá službami OS a to především principy dynamických knihoven. Výklad začíná pro srovnání od MS-DOSu, který sice náleží k již ustupujícím OS, avšak stále je poměrně dost používaný, zejména pro jednodušší regulační nebo monitorovací programy, protože se v něm dají bez obtíží obsluhovat fyzické periferie.
9.1 Principy volání služeb systému Programy se na OS obracejí prostřednictvím jeho služeb. Mechanismus jejich volání musí zaručovat přenositelnost programů nejen mezi počítači vybavených stejným operačním systémem, ale také mezi jednotlivými verzemi operačního systému. 9.1a Služby MS-DOSu Každý počítač PC obsahuje jednoduchý operační systém, který je pevně uložený v paměti ROM na adresách 0F000h:0 až 0F000h:0FFFFh. Nazývá se BIOS (Basic Input Output System) a realizuje obsluhu primárních jednotek základní desky počítače, a proto závisí na jejím konkrétním typu. MS-DOS navazuje na BIOS a rozšiřuje jeho možnosti. MS-DOS a BIOS nabízejí svoje služby prostřednictvím vstupních bodů uložených v paměti RAM na vybraných fyzických adresách od 0h do 3FFh. Tam se nacházejí přerušovací vektory procesorů řady 86, jejímž původním cílem byla obsluha přerušení od vnějších periférií, která probíhala takto: 1. Periferní zařízení žádající o přerušení vydalo signál INTR. 2. V okamžiku, když procesor přerušil svůj program, oznámil to signálem INTA. 3. Periferní zařízení poslalo procesoru po datové sběrnici číslo v rozsahu 0 až 255 udávající index obslužného programu přerušení. 4. Procesor si z tabulky vektorů přerušení přečetl adresu podprogramu a ten provedl. Popsaný způsob dovoloval, aby periférie specifikovaly žádaný program, ale neumožňoval rozlišit mezi několika požadavky. Budou-li o přerušení žádat dvě zařízení najednou, kterému z nich se má vyhovět? Počítač PC obsahuje kvůli tomu přídavný obvod - řadič přerušení, který na svých vstupech přijímá požadavky od jednotlivých periférií. Ty zpracovává podle jejich priority a jim přiřazené přerušovací vektory předává procesoru podle priorit. Hovoříme-li o přerušení, musíme proto rozlišovat zda specifikujeme: • číslo vektoru přerušení, čili index vektoru v tabulce přerušení. Každý vektor má formát pointru typu far v 16-ti bitových OS, tj. tvoří ho 4 bytové číslo určující segment:offset. Například přerušovací vektor s indexem 8 (INT 8), uložený na adrese 0:20h (=4*8) definuje podprogram pro zpracování přerušení od časovače. • číslo žádosti o přerušení, tj. vstupní signál řadiče přerušení. Například zmíněné přerušení časovače (INT 8) se vyvolá, pokud přijde signál na vstup 0 řadiče přerušení, ve schématech často označený jako IRQ 0. V dalším textu se bude hovořit výhradně o vektorech přerušení. Podprogramy jimi určené lze vyvolat i instrukcí procesoru INT, a proto se jim často říká softwarová přerušení. Některé z nich generuje sám procesor. Třeba INT 0 se vyvolá při celočíselném dělení 0 a OS na něj "' reaguje strukturovanou výjimkou EXCEPTION_INT_DIVIDE_BY_ZERO. Vstupy do tabulky přerušení, které nepoužívá procesor, slouží v MS-DOSu pro uložení adres vstupních bodů služeb operačního systému MS-DOSu a BIOSu. Nejznámějším vektorem přeru"'
Procesor ve Win32 pracuje v chráněném módu, v němž také existují přerušovací vektory, ale s jinou strukturou než v MS-DOSu. Ta se mimo jiné liší i vyšším počtem vektorů pro výjimky generované procesorem, z nichž pak OS Win32 vytváří strukturované výjimky. Nicméně, INT 0 hlásí dělení nulou i v chráněném módu.
200
šení je INT 21H, vstupní bod hlavních služeb MS-DOSu. Ten zprostředkovává řadu dílčích funkcí rozlišovaných obsahem registru AH a dalšími parametry.
Paměť RAM Řadič přerušení IRQ0
0:0000h
Přerušovací vektor 0
0:0004h
Přerušovací vektor 1
0:0020h
Přerušovací vektor 8
0:0058h
Přerušovací vektor 16h
0:0084h
Přerušovací vektor 21h
0:03FCh
Přerušovací vektor 0FFh
Výjimka dělení 0
Tabulka vektorů
Pomocná paměť dat BIOSu. Vnitřní data DOSu.
MS-DOS Vstupní bod podprogramu služby
Uživatelský program MOV AH, 2Ah ;číslo funkce = čti čas INT 21h ;volání služby DOSu
ROM BIOS
MOV AH, 0 INT 16h
;číslo funkce = čti znak ;volání služby BIOSu
Vstupní bod podprogramu služeb klávesnice Vstupní bod podprogramu obsluhy přerušení od časovače Obr. 9-1 Volání služeb MS-DOSu a ROM-BIOSu
Uživatelské programy na tyto služby odkazují pouze pomocí instrukcí INT, takže volají podprogramy specifikované pointry uloženými na absolutních adresách přerušovacích vektorech. Ty se nemění s verzí operačního systému. Přenos dat se řeší také přes pevné adresy, jako například nastavení čísel portů sériových vstupů a výstupů. K tomu slouží tabulky umístěné hned za přerušovacími vektory.
201
9.1b Dynam ické knihovny Win32 Win32 používají přerušovací vektory procesoru pouze pro strukturované výjimky. Všechny jejich služby se volají mechanismem dynamických knihoven. Ty představují velmi důležitý prvek, na němž stojí většina samotného OS. Může je vytvářet i uživatel a doplňovat možnosti Windows. Dynamické knihovny se liší od statických především tím, že se neukládají do souboru aplikace, ale připojují se až při spuštění aplikace a to vždy celé. Naproti tomu statické knihovny představují sestavu relativných modulů (OBJ), z níž se vybírají pouze potřebné funkce, které se připojují k programu už ve fázi jeho sestavovaní. Dynamická knihovna se svoji strukturou blíží běžnému programu. Její funkce lze z procesu volat dvěma odlišnými způsoby. 1. Funkce dynamické knihovny, která se potřebuje, se specifikuje jako součást programu. Knihovna, která ji obsahuje, se připojí během vytváření procesu ze souboru aplikace (load-time dynamic linking). Pokud tato knihovna chybí, nahlásí se chyba. Takové použití dynamické knihovny se označuje jako automatické nebo implicitní. (Někdy se označuje i matoucím pojmem statické.) 2. Proces si připojí dynamickou knihovnu až v okamžiku, kdy ji potřebuje (voláním LoadLibrary). Teprve pak může s knihovnou pracovat - číst v ní uložená resource nebo volat její funkce přes pointry, zjištěné voláním GetProcAddress. Takové zacházení s dynamickou knihovnou se nazývá explicitní nebo ne-automatické. (V některých publikacích se označuje i jako dynamické). Implicitní použití představuje jednoduchou záležitost pro program, ale komplikovanou pro OS. Část New Header, uvedená v kapitole 4.8 na straně 119, a nacházející se na začátku každého souboru aplikace, obsahuje i tabulku použitých funkcí z implicitně připojených dynamických knihoven.
EXE header STUB NEW header
Hlavička kompatibilní s MS-DOS programy Kód MS-DOS programu vypisující obvykle hlášení "Program vyžaduje MS-Windows" . Seznamy pomocných datových zdrojů
Popis prvků aplikace
Kód programu
Tabulka použitých funkcí z DLL knihoven
Tabulka adres systémových služeb Operace programu
RESOURCE
Datové zdroje programu, např. ikony, bitové mapy, data pro vytvoření menu, dialogů a pod.
Obr. 9-2 Tabulky systémových slu eb a knihoven Tabulka obsahuje jméno příslušné dynamické knihovny a identifikaci z ní použitých funkcí. Každá funkce může být specifikovaná buď svým jménem anebo interním pořadovým číslem platným pouze v rámci svojí knihovny. 202
Funkce z implicitně připojených dynamických knihoven se volají přes nepřímé skoky, jejichž cílové adresy definuje další tabulka, tentokrát umístěná na začátku kódu programu. Tu vyplňuje OS až v okamžiku vytváření procesu podle toho, kde se právě nacházejí dynamické knihovny.
Hlavička aplikace
Seznam použitých služeb Jméno služby OS
Knihovna obsahující službu
ChooseFont
COMMDLG
MessageBox
USER
GlobalAlloc
KERNEL
Tabulka adres služeb ( 32 bitové adresy ) ADR_CHOOSEFONT: Adresa ChooseFont ADR_MESSAGEBOX: Adresa MessageBox
Kód programu
ADR-GLOBALALLOC:
Adresa GlobalAlloc
Tabulka skoků na nepřímé adresy ChooseFont:
JMP [ADR_CHOOSEFONT]
MessageBox:
JMP [ADR_MESSAGEBOX]
GlobalAlloc:
JMP [ADR_GLOBALALLOC]
Operace programu
Kód knihovny USER
;Volání služby CALL MessageBox
Kód služby MessageBox
Obr. 9-3 Principu volání funkcí z automatických dynamických knihoven Použití funkce MessageBox ukazuje Obr. 9-3. Proces ji volá pomocí běžné instrukce procesoru CALL, jejíž cílová adresa směřuje na instrukci nepřímého skoku (JMP [...] ), která si z tabulky použitých služeb přečte adresu, udávající začátek MessageBox, a na tu předá řízení programu (tj. uloží ji do registru IP). Zůstala jen otázka, kde se MessageBox bude nacházet. Odpověď pro Win32 zní - v adresovém prostoru procesu, který ji zavolal. Přestože se v předchozích kapitolách, pro zjednodušení výkladu, tvrdilo, že proces má k dispozici 4 GB virtuální paměti, není to tak docela pravda.
203
Část adresového prostoru blokují veškeré prvky, které aplikace potřebuje - nejen vlastní data procesu, ale také data všech jím používaných dynamických knihoven. Ta se pro každý proces vytvářejí unikátní. Pouze lokální data se ukládají na zásobník procesu či vlákna, z něhož byla volaná, protože dynamická knihovna nemá svůj zásobník. Do adresového prostoru (viz. model flat na str. 35) se navíc mapují kódy všech používaných dynamických knihoven, tj. zhruba celé jádro operačního systému. Adresový prostor procesu A DLL data procesu A DLL
Stránkovaná paměť DLL data procesu B Datový adresový prostor procesu B
Obr. 9-4 Mapování dynamické knihovny do adresového prostoru procesu Veškeré bloky se nacházejí ve stránkované paměti OS. Ta se realizuje pomocí paměti RAM a disku. Ve velmi hrubém přiblížení ji lze pokládat za jakýsi seznam určující obsah a umístění dílčích stránek paměti. Seznam určující obsah stránkované paměti Kód A
Kód B
Data procesu B DLL
B
Data procesu A
A Disk
B Soubor B.EXE
B DLL
B RAM
A
Soubor A.EXE Pomocný soubor (Win386.swp)
Soubor Knihovna.DLL
Discardable element Obr. 9-5 Náèrt principu realizace stránkované pamìti OS podle seznamu vytváří mapování pro jednotlivé procesy, dále do fyzické paměti RAM nahrává právě potřebné elementy a naopak nepoužívané elementy ukládá dočasně na disk. Kódy dynamických knihoven, v daném okamžiku nepotřebné, se však pouze vymažou, neboť se při běhu programu nemění a dají se obnovit z originálních souborů. Stránky paměti dovolující zahození se označují jako discardable. Těmi jsou zpravidla všechny, které obsahují kód
204
programu, a dále většina resource, jako ikony, bitmapy a podobně. Jejích opakem jsou data vytvořená programem, která vyžadují odložení do pomocného souboru pro opětovné obnovení. Vytváří-li OS proces, který používá dynamické knihovny implicitně, neznamená to, že nahraje do paměti RAM všechny soubory. Pouze o nich uloží adekvátní informace do stránkované paměti a na jejich základě vyplní příslušnou tabulku odkazů na funkce. Soubory dynamických knihoven se fyzicky načítají do paměti RAM teprve až v okamžiku, kdy se skutečně zavolá nějaká jejich funkce. OS dočasně zablokuje všechny soubory zařazené do stránkované paměti a nedovoluje měnit jejich obsah. Blokování zruší až v okamžiku, kdy skončí všechny procesy, které je používají. Tuto vlastnost třeba mít na zřeteli zejména při testování dynamických knihoven. Dynamické knihoven skýtají výhodu úspory diskového prostoru, protože zaujímají na něm neustále konstantní místo bez ohledu na to, jestli s nimi nějaký proces pracuje nebo ne, protože se nekopírují na swap. Šetří i paměť, ale pouze tehdy, když se najednou volají z více procesů. Nemají totiž interní strukturu rozčleněnou na zcela nezávislé bloky jako statické knihovny, a proto se do RAM prakticky nahrávají vždy celé. Kvůli tomu se většinou vytvářejí jako malé soubory realizující dílčí operace.
9.2 Vytvoření dynamické knihovny ve Win32
9.2a
Vstupní bod dynamické knihovny
Dynamické knihovny používají nejčastěji přípony DLL, Dynamic-Link Library, ale i jiné - EXE a DRV pro drivery, FON pro uložení fontů a další. Jejich zdrojový text ve Win32 se příliš neliší od normálních programů, pouze v něm chybí vstupní bod WinMain. Ten nahradil DllEntryPoint, nepovinný podprogram pro inicializaci. OS ho zavolá pokaždé, když se k dynamické knihovně připojuje nebo odpojuje proces nebo vlákno. BOOL WINAPI DllEntryPoint( HINSTANCE hinstDLL, // handle instance dynamické knihovny DWORD fdwReason, // důvod proč se funkce volá LPVOID lpvReserved ); // další informace pro případ připojení k procesu Funkce vrací TRUE při úspěšné inicializaci, v opačném případě FALSE. Její parametry udávají: hinstDLL - handle instance dynamické knihovny, který má podobný význam jako jeho analogie v aplikacích. Lze pomocí něho přistupovat do modulu dynamické knihovny. fdwReason - určuje příčinu volání funkce a nabývá jedné z následujících hodnot: DLL_PROCESS_ATTACH - implicitní nebo explicitní připojení k procesu, čehož lze využít tohoto k inicializaci datového segmentu přiděleného danému procesu. DLL_THREAD_ATTACH- daný proces vytvořil nové vlákno. Při té příležitost OS volá všechny DllEntryPoint ve všech dynamických knihovnách připojených k danému procesu. DLL_THREAD_DETACH - odpojení od vlákna. DLL_PROCESS_DETACH - odpojení od procesu. lpvReserved - přídavná informace mající význam při připojení a odpojení k procesu: • při připojení k procesu (DLL_PROCESS_ATTACH) se rovná NULL, pokud byla knihovna nahraná explicitně (LoadLibrary), jinak je různá od NULL; • při odpojení od procesu (DLL_PROCESS_DETACH) se rovná NULL, pokud byla knihovna nahraná explicitně (LoadLibrary), jinak je různý od NULL. Rozdíl mezi připojením k procesu a k vláknu vychází z faktu, že vlákna sdílejí statická data se svým procesem. Připojí-li se proces, znamená to, že existují nová statická data dynamické knihovny, která lze inicializovat, je-li to potřeba. Naproti tomu, vytvoří-li se v procesu další 205
vlákno, potom bude při volání funkcí dynamických knihoven používat tatáž statická data jako jeho proces. Pokud nějaká funkce dynamické knihovny realizuje operace, či sérii operací, vyžadujících interní uchovávání dat ve statických proměnných, pak by ve vícevláknovém procesu docházelo k vzájemnému přepisování těchto údajů jednotlivými vlákny. Tento problém řeší TLS - Thread Local Storage (lokální buňka vlákna). Prostor pro její uložení vznikne po zavolání TlsAlloc. Pomocí TlsSetValue pak lze do TLS uchovat jednu hodnotu a tu opět číst TlsGetValue. Například se v dynamické knihovně může alokovat paměť a získaný pointer uschovat do TLS. Obě operace, zápis a čtení hodnoty z TLS, závisejí na vlákně, z něhož se volají, a údaje uložené různými vlákny se nepřepisují. Každé vlákno procesu má přidělenou právě jednu TLS buňku v každé TLS paměti, kterou si proces vytvořil. Je-li potřeba TLS, pak ji lze v DllEntryPoint inicializovat třeba takto: static DWORD dwTlsIndex = 0xFFFFFFFF; /* proměnná dynamické knihovny pro uložení identifikace vytvořené TLS */ BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { LPVOID lpvData; // Pomocná proměnná, pointer typu void switch (fdwReason) // Důvod volání DllEntryPoint { case DLL_PROCESS_ATTACH: // Připojení k procesu dwTlsIndex = TlsAlloc(); // Vytvoření TLS. OS vrátí její identifikaci if (dwTlsIndex == 0xFFFFFFFF) // Selhalo vytvoření TLS return FALSE; // Knihovnu nelze připojit // break vynechán záměrně - nutno alokovat lokální paměť i pro hlavní vlákno procesu ! case DLL_THREAD_ATTACH: // Připojení k vláknu lpvData = (LPVOID) LocalAlloc( // Alokace lokální paměti vlákna LPTR, // Atribut = přidělit paměť a vynulovat ji 1024); // Požadovaná délka paměti TlsSetValue(dwTlsIndex, lpvData); // Uložíme pointer do TLS break; case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: if(dwTlsIndex == 0xFFFFFFFF) break; lpvData = TlsGetValue(dwTlsIndex); // Přečteme pointer na lokální paměť if (lpvData != NULL) { LocalFree((HLOCAL) lpvData); // Uvolníme lokální paměť TlsSetValue(dwTlsIndex, NULL); // Paměť uvolněná } if( fdwReason != DLL_PROCESS_DETACH) break; TlsFree(dwTlsIndex); // Zrušíme TLS dwTlsIndex = 0xFFFFFFFF; // Pro jistotu nastavíme příznak zrušení break; } return TRUE; } Poznámka: Použitá služba LocalAlloc pracuje jako GlobalAlloc, protože v prostředí Win32 není rozdíl mezi lokálním a globálním heap. Obě alokace se liší výhradně jinými atributy - GlobalAlloc nabízí o něco širší možnosti než LocalAlloc. Uvedená inicializace nebude fungovat, pokud se dynamická knihovna připojí až po vytvoření vlákna, což může nastat při její explicitním nahraní pomocí LoadLibrary (run-time linking ) v době, kdy vlákno už běží. Pro tyto případy se hodí kontrolní funkce v knihovně. Lze pro ni využít toho, že TLS buňky jsou po TlsAlloc inicializované na NULL.
206
/* Pomocná funkce dynamické knihovny navazující na předchozí ukázku */ void InitilizeTLS() { LPVOID lpvData; lpvData = TlsGetValue(dwTlsIndex); if (lpvData == NULL) lpvData = (LPVOID) LocalAlloc(LPTR, 1024); TlsSetValue(dwTlsIndex, lpvData); }
// // // //
Čteme obsah TLS buňky vlákna Buňka prázdná Alokujeme lokální paměť Uložíme pointer
Bude-li dynamická knihovna mít jméno DLLTEST.DLL, pak uvedenou funkci lze při explicitním nahrání volat takto: HINSTANCE hLibrary; FARPROC lpFunc; hLibrary = LoadLibrary(„DLLTEST.DLL“); lpFunc = GetProcAddress(hLibrary, „InitilizeTLS“); (*lpFunc) ();
9.2b
// Připojíme dynamickou knihovnu // Získáme adresu funkce // Voláme funkci
Kód programu
Program dynamické knihovny se píše podobně jako jiné aplikace. Všechny funkce knihovnou nabízené se musejí specifikovat jako WINAPI (= CALLBACK), aby se přeložily ve tvaru vhodném pro volání zvnějšku. Vytvořme například v dynamické knihovně, pro vyzkoušení práce s TLS, interní pole šestnácti čísel double a k němu dvě funkce pro čtení a zápis prvku a pomocnou funkci TretiMocnina. Program navazuje na předchozí kód DllEntryPoint a InitilizeTls. /****************************** DLLTEST.CPP ****************************/ #include <windows.h> #include "dlltest.h" /* Sem vložte uvedené kódy DllEntryPoint a InitilizeTLS */ void WINAPI UlozCislo(double cislo, BYTE index) // Zapiš číslo do pole knihovny { LPVOID lpvData = TlsGetValue(dwTlsIndex); // Načteme adresu lokální paměti vlákna if( lpvData!=NULL && index<16 ) ( (double *)lpvData )[index] = cislo; } double WINAPI CtiCislo(BYTE index) // Čti číslo z pole v knihovně { LPVOID lpvData = TlsGetValue(dwTlsIndex); // Načteme adresu lokální paměti vlákna return ( lpvData!=NULL && index<16 ) ? ((double *)lpvData)[index] : 0; } double WINAPI TretiMocnina(double cislo) { return cislo*cislo*cislo; } Použití dynamické knihovny vyžaduje header soubor. Jelikož jde o knihovnu, je vhodné potlačit v něm name mangling pomocí popisu extern "C" (viz. kapitola 1.2). Předběhneme mírně výklad a do header souboru umístíme i deklarace funkcí ReadNumber a WriteNumber, ekvivalentů UlozCislo a CtiCislo lišících se pouze jinými identifikátory. Změna jména funkce by se hodila třeba v případě nabídky knihovny do ciziny. Přidané deklarace v header souboru neškodí, ale zatím se nedají použít, protože k nim neexistují odpovídající definice. Jejich uplatnění si ukážeme až v následujících odstavcích.
207
/****************************** DLLTEST.H ****************************/ extern "C" { // Potlačíme name mangling void WINAPI InitilizeTLS(); double WINAPI TretiMocnina(double polomer); void WINAPI UlozCislo(double cislo, BYTE index); double WINAPI CtiCislo(BYTE index); void WINAPI WriteNumber(double cislo, BYTE index); // Anglické ekvivalenty funkcí double WINAPI ReadNumber(BYTE index); } Výsledný program lze už přeložit, ale dynamickou knihovny stále nelze použít zvnějšku, protože chybí popis nutný pro vytvoření příslušné tabulky v hlavičce knihovny.
9.2c
Soubor DEF - Module Definition File
V kapitole 5.4 byl uvedený jednoduchý DEF soubor postačující pro většinu Win32 aplikací: NAME WINAPI DESCRIPTION 'Ukázková úloha pro Windows API' Dynamické knihovny vyžadují změnu NAME na popis LIBRARY. LIBRARY DLLTEST DESCRIPTION 'Ukázková dynamická knihovna pro předmět PJR' kde název knihovny musí souhlasit se jménem jejího souboru bez přípony. Do DEF souboru se podle potřeby přidávají další sekce, třeba pro určení velikosti paměti vyhrazené pro alokace: HEAPSIZE 100000 Největší význam pro dynamické knihovny má sekce EXPORTS, na jejímž základě se vytváří tabulka nabízených funkcí. Její základní syntaxi vypadá takto: EXPORTS entryname [= internalname] [@ordinal] kde parametry mají následující význam (závorky [] určují nepovinný argument): entryname - jméno funkce, pod nímž se bude volat zvnějšku; internalname - identifikátor funkce ve zdrojovém kódu dynamické knihovny; @ordinal - unikátní interní identifikační číslo platné v rámci knihovny (ordinal number), přes nějž lze odkazovat na funkce při explicitním nahrávání knihovny. Definuje se pro zrychlení práce jako alternativa jména. Obvykle se přiřazuje v pořadí od 1, ale není to nutné a čísla lze přidělit libovolná, avšak jedinečná v rámci knihovny. Při více popisech se klíčové slovo EXPORT může uvést jen na úvodní řádce sekce, nebo těsně před ní, a na dalších se již nemusí opakovat. Soubor DLLTEST.DEF specifikuje čtyři funkce, přičemž definuje pro UlozCislo a CtiCislo alternativní vnější názvy. /****************************** DLLTEST.DEF ****************************/ LIBRARY DLLTEST DESCRIPTION 'Ukázková dynamická knihovna pro předmět PJR' HEAPSIZE 100000 EXPORTS InitilizeTLS @1 WriteNumber = UlozCislo @2 ReadNumber = CtiCislo @3 TretiMocnina @4 Nyní máme již vše potřebné. Soubory DLLTEST.CPP a DLLTEST.DEF lze zařadit do projektu překladače a výsledný program přeložit.
208
9.2d
Zjednodušení kódu dynamické knihovny
Příklad v předchozí části demonstroval řadu možností dynamických knihoven. Ty však nebývají zpravidla potřeba všechny a program lze zjednodušit vynecháním některých operací: 1. TLS paměť je nutná jen tehdy, když se knihovna používá z více vláken vytvořených v rámci jednoho procesu a její exportované funkce si potřebují vzájemně předávat data přes statické proměnné, jako třeba uložit si handle vytvořeného okna a podobně. V opačném případě, který tvoří častější situaci, se TLS může vynechat. 2. Vstupní bod DllEntryPoint slouží většinou buď pro inicializaci TLS anebo statických dat knihovny. Nejsou-li podobné inicializace potřeba, lze DllEntryPoint vynechat. Dynamické knihovny ve Win32 nepotřebují povinné inicializační operace, na rozdíl jejich protějšků ve Win16. 3. Sekce EXPORTS dovoluje změnu názvu funkce. Pokud podobné přejmenování není třeba, vytvoření sekce EXPORTS lze nechat na překladači. Uvede-li se v Borland C++ klíčové slovo _export v hlavičce funkce, pak se EXPORTS sekce sestaví automaticky při překladu. Klíčové slovo _export navíc nahradí i popis WINAPI a způsobí, že se funkce automaticky přeloží do tvaru vhodného pro dynamické knihovny. Poznámka: Ve Visual C++ a Borland C++ Builder existují analogie k _export ve tvaru: __declspec(dllexport). 4. Použije-li se automatické vytvoření EXPORTS sekce pøekladaèem, lze soubor DEF vynechat celý. Překladač si vytvoří nutný popis LIBRARY sám ze jména souboru aplikace. V předchozím příkladu změníme kód funkcí tak, aby používaly vnější pole, na něž dostanou pointer. Tím odpadne složitá inicializace TLS a nutnost psát DllEntryPoint. Vzdáme-li se také možnosti změny jmen a popis WINAPI nahradíme klíčovým slovem _export, pak se zbavíme nutnosti psát EXPORTS a zbytek DEF souboru lze vynechat. Zůstane nám několik řádek kódu. /***********;******************* DLLTESTN.H ****************************/ extern "C" { // Potlačíme name mangling double _export TretiMocnina(double polomer); void _export UlozCislo(double * pole, double cislo, BYTE index); double _export CtiCislo(double * pole, BYTE index); } /****************************** DLLTESTN.CPP ****************************/ #include <windows.h> void _export UlozCislo(double * pole, double cislo, BYTE index) // Zapiš číslo do pole { if(index<16) pole[index] = cislo; } double _export CtiCislo(double * pole, BYTE index) { return (index<16) ? pole[index] : 0; }
// Čti číslo z pole
double _export TretiMocnina(double cislo) { return cislo*cislo*cislo; } Dynamická knihovna nemusí obsahovat jen kód programu, ale také resource. Ty mohou dokonce být i její jediným prvkem. Podobné knihovny se vytvářejí velmi často. Například: /****************** Res2DLL.rh **********************/ #define ID_POZDRAV 300 /****************** Res2DLL.rc **********************/ BITMAPA BITMAP { '42 4D 76 08 00 00 00 ...... '00 00 00' } // bitová mapa KURZOR CURSOR { '00 00 02 .... FF FF' } // Kurzor myši IKONA ICON { '00 00 01 ..... FF F8 FF FF' } // Ikona pro naši aplikaci STRINGTABLE { ID_POZDRAV, "Zdraví Vás resource!" } // Pole řetězců Soubor DEF není zde vůbec potřeba, protože resource se automaticky zařazují mezi exportovatelné položky.
209
9.2e
Volání funkcí dynamické knihovny
Dynamickou knihovnu lze připojit implicitně nebo explicitně. Při implicitním připojení se do projektu programu, který knihovnu požívá, zařadí odpovídající importovaná knihovna, viz. kapitola 4.6, pro uvedený příklad DLLTEST.LIB. Tu z dynamické knihovny vytvoří Import Librarian, tvořící běžnou součást překladačů a zpravidla automaticky volaný po vzniku souboru dynamické knihovny, takže výsledkem překladu jejího zdrojového kódu bývají dva soubory *.DLL a *.LIB. Soubor importované knihovny *.LIB má sice stejnou příponu jako statické knihovny, ale na rozdíl od nich neobsahuje kódy funkcí, ale pouze reference nutné k implicitnímu připojení dynamické knihovny. # Volání funkcí z implicitně připojených dynamických knihoven se na úrovni zdrojového textu neliší od běžných funkcí. Lze je využít v GUI i v Console aplikacích. Všechny funkce se samozřejmě volají pod jmény, které se jim v DEF souboru přidělila pro vnější použití. #include <windows.h> #include <stdio.h> #include "dlltest.h" void main(void) { int i; for(i=0; i<16; i++) WriteNumber( TretiMocnina(i),i ); for(i=0; i<16; i++) if( ReadNumber(i)!=TretiMocnina(i) ) printf("Chyba!"); else printf("%lg ",ReadNumber(i)); Sleep(5000); // Pauza 5 vteřin před zavřením Console okna } Úpravu uvedeného programu na více vláken, třeba s použitím objektu Vlakno, definovaného v kapitole 8.4, a tím vyzkoušení funkce TLS, ponechám čtenáři. Ukažme si místo toho program používající stejnou dynamickou knihovnu explicitně. Explicitní připojení zajistí služba: HINSTANCE LoadLibrary( LPCTSTR lpszLibFile ); Její parametr lpszLibFile udává jméno souboru s příponou EXE nebo DLL, které může obsahovat úplnou. Pokud cesta není, hledá se v adresářích podle následujícího postupu: 1. Adresář, ze kterého se nahrála aplikace. 2. Okamžitý adresář. 3. Systémový adresář Windows (cestu k němu lze zjistit službou GetSystemDirectory). 4. Adresář Windows (cestu k němu lze zjistit službou GetWindowsDirectory).. 5. Adresáře uvedené v PATH. Služba LoadLibrary vrací instanci dynamické knihovny, zadávanou jako argument při práci s knihovnou. Instanci dynamické knihovny lze v argumentech funkcí používat také všude, kde je uvedený typ HMODULE. HINSTANCE s ním hodnotově splývá a liší se od něho jen jiným teoretickým významem. Službě LoadLibrary se podobá GetModuleHandle, která však nepátrá na disku, ale hledá v již nahraných modulech do paměti. Explicitní dostup se nejvíce používá pro dynamické knihovny s resource prvky. Knihovny se nahrají LoadLibrary a jednotlivá dílčí resource se z nich načítají službami, které začínají Load... - LoadAccelerators, LoadBitmap, LoadCursor, LoadIcon, LoadMenu, LoadString. S knihovnou Res2DLL.DLL z předchozí části lze pracovat třeba takto: #include <windows.h> #include <stdio.h> #include "dlltestn.h" #
Importované knihovny nahradily dříve používaný popis IMPORTS v souboru DEF, vyžadovaný rannými verzemi kompilátorů, v němž se uváděl seznam všech vnějších funkcí z implicitně připojovaných dynamických knihoven. Většina dnešních překladačů popis IMPORTS již nezná a hlásí ho jako syntaktickou chybu.
210
#include "res2dll.h" void main(void) { char buf[256]; HINSTANCE hreslib = LoadLibrary("Res2DLL.DLL"); // načteme knihovnu LoadString(hreslib, ID_POZDRAV, buf, sizeof(buf) ); // přečteme si z ní řetězec printf("Nacteny retezec: %s\n",buf); // vytiskneme ho Sleep(5000); // Pauza 5 vteřin před zavřením Console okna } Umístění textů do dynamické knihovny dovoluje vytvářet vícejazyčné verze programů. Profesionální aplikace mívají často veškeré texty, které vypisují, umístěné v dynamické knihovně. Její výměnou mohou přepínat jednotlivé jazykové verze programu. Volání funkcí z explicitně připojené dynamické knihovny je již o něco těžší, protože ho komplikuje předávání parametrů. Pro každou funkci se musí deklarovat vhodný pointer na funkci se správnými popisy parametrů, aby překladač věděl jak volání provést. Navíc nelze spoléhat na automatickou konverzi parametrů, příslušné převodní algoritmy překladače nemusejí dobře rozeznat typy proměnných deklarované v pointru na funkci. Vlastní zjištění adresy funkce z dynamické knihovny provede služba: FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpszProc ); kde hModule udává dynamickou knihovnu a představuje handle vrácený buď LoadLibrary nebo GetModuleHandle. Parametr lpszProc specifikuje řetěze udávající jméno funkce. Místo něho lze použít přidělené identifikační číslo funkce v modulu DEF. V tom případě je číslo v dolních 16-ti bitech lpszProc a horních 16 bitů se musí rovnat 0. Služba GetProcAddress vrací adresu funkce nebo NULL, pokud se nenašla. Analogie předchozího programu upravená pro explicitní volání by vypadala takto: #include <windows.h> #include <stdio.h> /* #include "dlltest.h" - není potřeba, protože explicitní připojení dynamické knihovny potřebuje jiné deklarace.*/ HINSTANCE hlibDllTest=NULL;
// instance dynamické knihovny
/* Deklarace tří proměnných typu pointer na funkci s parametry a návratovou hodnotou */ void (FAR * lpWriteNumber) (double cislo, BYTE index); double (FAR * lpReadNumber) (BYTE index); double (FAR * lpTretiMocnina) (double polomer); FARPROC NactiAdresu(char * jmeno) throw(const char *) // Zjištění adresy funkce { FARPROC lpproc; if(hlibDllTest==NULL) throw("DLLTEST.DLL"); // Výjimka, pokud není nahraná knihovna lpproc = GetProcAddress(hlibDllTest, jmeno); // Zjisti adresu if(lpproc==NULL) throw(jmeno); // Výjimka, pokud se funkce nenašla else return lpproc; // Vracíme adresu nalezené funkce }
211
void main(void) { int i; double mocnina; // Pomocná proměnná zavedená kvůli správné konverzi typu try { hlibDllTest = LoadLibrary("DLLTEST.DLL"); // Nahrání knihovny (FARPROC) lpWriteNumber = NactiAdresu("WriteNumber"); // Zjištění adresy ze jména (FARPROC) lpReadNumber = NactiAdresu("ReadNumber"); (FARPROC) lpTretiMocnina = NactiAdresu( (char *)(4) ); // Zjištění adresy z čísla for(i=0; i<16; i++) { mocnina=(* lpTretiMocnina)((double) i); (*lpWriteNumber)(mocnina,(BYTE) i); } for(i=0; i<16; i++) { mocnina = (* lpReadNumber)( (BYTE) i ); if( mocnina!=(* lpTretiMocnina)((double) i) printf("Chyba!"); else printf("%lg ",mocnina); } } catch (const char * jmeno) // Výjimka, nebude-li nalezená knihovna. { printf("Nenalezen objekt: %s\n", jmeno); return; // konec aplikace } if(hlibDllTest!=NULL) FreeLibrary(hlibDllTest); // Uvolnění knihovny. Sleep(5000); // Pauza 5 vteřin před zavřením Console okna. } Poznámka: Ve Visual C++ se uvedené pointry na funkci musejí napřed definovat jako typedef a teprve od nich odvodit proměnné. Jejich typ se potřebuje u NactiAdresu pro konverzi výsledku, protože ve Visual C++ nejsou dovolené konverze typu na levé straně výrazu. typedef void (FAR * LPWriteNumber) (double cislo, BYTE index); // definice typu LPWriteNumber lpWriteNumber; // definice proměnná lpWriteNumber = (LPWriteNumber) NactiAdresu("WriteNumber"); // přetypování
9.3 Odlišnosti dynamických knihoven ve Win16 Zdrojové kódy programy pro dynamické knihovny Win16 vyžadují některé odlišnosti, z nichž hlavní jsou tyto: • Má-li Win16 dynamická knihovna datový segment, pak je sdílený všechny procesy, které s ní pracují. Jinými slovy, Win16 dynamická knihovna datový segment buď vůbec nemá, nebo vlastní právě jeden a ten pak představuje společnou paměť pro všechny aplikace, které ji používají. Některé Win16 programy, zejména databázového charakteru, toho využívaly pro výměnu dat mezi aplikacemi. Takové programy nelze spustit ve Win32. • Datový segment Win16 dynamické knihovny má maximálně 64 kB. Ve Win32 je jeho délka omezená pouze velikostí volného adresového prostoru. • Kód dynamické knihovny není mapovaný do adresového prostoru procesu, ale leží vně něho, což zvyšuje riziko narušení OS. • Win16 dynamické knihovny zpravidla vyžadují soubor DEF, v němž se popisuje charakter programových a datových segmentů. Ve Win32 není nic takového potřeba, protože jejich paměťový model FLAT není segmentovaný, ale spojitý.
212
• Dynamická knihovna ve Win16 má povinný vstupní bod LibMain, v němž musí odemknout svůj datový segment a tím povolit jeho dočasné odstranění z adresového prostoru: int FAR PASCAL LibMain( HINSTANCE hInstance, WORD wDataSegment, WORD wHeapSize, LPSTR lpszCmdLine ) { if ( wHeapSize != 0 ) UnlockData( 0 ); // Je-li heap, pak je i datový segment return 1; // knihovna inicializována. } • Vstupní bod LibMain se ve Win16 volá jen při prvním nahrání dynamické knihovny do paměti. Nereflektuje na připojení či odpojení dynamické knihovny k vláknu a k procesu. • Win16 dynamické knihovny mají nepovinnou funkci WEP volanou při odstranění knihovny z paměti: int FAR PASCAL WEP ( int bSystemExit ) { return 1; } Poznámky: 1. Některé překladače dovolují ve zdrojovém kódu volbu jména pro DllEntryPoint, například ve Visual C++ deklarace APIENTRY před identifikátorem funkce. DllEntryPoint se pak klidně může jmenovat LibMain, avšak v tom případě nejde samozřejmě o LibMain, ale DllEntryPoint. 2. Operace UnlockData je ve Win32 nedefinovaná. Existuje ale jiné "Lock" funkce blokující uvolnění bloku z paměti, např. GlobalLock a GlobalUnlock.
213
10 Závěr 10.1 Další zajímavá témata Win32 Měl jsem v plánu zařadit do skripta více témat a podrobnější popisy. Bohužel limit rozsahu mě nutí končit již zde, i tak jsem ho překročil. Přikládám aspoň výčet vynechaných oblastí. DDE protokol. Komunikace mezi aplikacemi může probíhat nejen zpráv nebo sdílené paměti. Lze ji navázat i DDE protokolem, pracujícím na principu Klient-Server. Takové propojení se často používá v aplikacích pro řízení, protože přístup na fyzické rozhraní počítače ve Win32 představuje extrémně složitou operaci a psaní driverů znamená úkol pro špičkového znalce Win32. Uživatelé si však mohou zakoupit nějakou profesionální aplikaci pro sběr dat, třeba RSLinx, který sbírá data z průmyslových automatů PLC. K němu lze pak DDE protokolem připojit vlastní aplikaci využívající jeho operace. Aplikace 2 DDE Klient
Zobrazení dat, řídící povely
Probíhající DDE komunikační topics RSLinx, DDE server
PLC
Sběr dat
Poèí ta è P C, Na dø í z en á sta n i ce
Aplikace 1 DDE Klient
PLC
Obr. 10-1 DDE protokol v øídících aplikacích Aplikace pracující s DDE se napřed zaregistruje jako klient, či server, nebo obojí. Je-li klientem, může připojit na vybraný server a vyžádat si komunikaci na některé jím nabízené téma (topic). Lze se napojit na jeden či více serverů a s každým z nich si vyměňovat data zaměřené na jedno nebo více témat, topics. Komunikace pro každý topic se skládá ze čtyř základních typů operací: • přenesení bloku dat na server (XTYP_POKE); • přečtení bloku dat ze serveru (XTYP_REQUEST); • spuštění či zastavení automatického přenosu (XTYP_ADVSTART / XTYP_ADVSTOP) a pokud běží, pak server sám posílá určený blok dat při každé změně jeho obsahu; • žádost, aby server provedl nějakou v něm zabudovanou operaci, XTYP_EXECUTE. DDE server v sobě obsahují mnohé aplikace, jenom namátkou lze uvést - Matlab, Excel, Word, Access. Často se uplatňuje pro Progman, tj. známý programový manažér Windows, jehož DDE služby dovolují vytvořit novou složku a ikony, což provádí každá instalace programu. Prostředí C++ Builder 3.0 nabízí tvorbu programu pro DDE komunikace v interaktivní podobě, takže napsání DDE klienta či serveru není složité. COM a OLE 2 Protokol označovaný jako OLE 2, Object Linking and Embedding, dovoluje propojení aplikací, linking, na úrovni využití jejich funkcí. Z cizí aplikace se při tom uplatní nejen operace, ale i 214
její prostředí, jako menu a vzhled. Tomu se říká zahloubení, embedding. OLE 2 se opírá o model COM, Component Object Model , který definuje jednotný styl objektového rozhraní. To určuje povinné objekty a jejich metody. Informace o nich se pak ukládají do předepsaných tabulek, nazývaných interface. Na rozdíl od DDE protokolu nemusí aplikace, k níž se chceme připojit, v daném okamžiku, běžet; Windows zařídí její spuštění. Komunikace začíná dotazováním, čili průzkumem tabulek OLE 2 objektů (jejich interface). Pomocí získaných údajů lze zavolat metody předepsané COM modelem, a tak lze zjistit potřebné informace o službách protějšku. Na základě toho lze využít jeho vybranou operaci ve vlastním programu. OLE 2 komplikují složité tabulky pro rozhraní, jejichž struktura není příliš čitelná. Naštěstí řada programovacích prostředí a objektových knihoven dovoluje poloautomatické vybudování nutných objektů pro OLE 2 a vytvoření tabulek a kódu pro požadované registrace, čímž se vše zredukuje aspoň na trochu únosnou míru. ActiveX a OCX OLE 2 znamená technologii pro propojení a zahloubení pomocí modelu COM. Naproti tomu, prvky ActiveX představují konkrétní uplatnění tohoto modelu. Za ActiveX se považují všechny objekty, které obsahují alespoň základní rozhraní definované COM modelem. ActiveX se nacházejí vně aplikace a představují jakési stavební elementy připojované přes OLE 2. Lze je vytvořit jak pro 16-ti tak pro 32 bitové prostředí. Ve Win32 nahrazují VBx prvky, Visual Basic Controls, které existují výhradně jako 16-ti bitové programy. Podmnožinu ActiveX prvků tvoří OCX, OLE Controls. Ty přidávají k minimálnímu interface požadovanému pro ActiveX další rozhraní modelu COM, až vznikají jakési specializované programy vybavené OLE 2 protokolem, které nejsou určené pro samostatné použití, ale slouží jen jiným aplikacím. Help soubory Windows podporují nápovědy. Vytvoření odpovídajících souborů není těžké, avšak vyžaduje na začátku větší úsilí pro pochopení jejich formátu. Klasickým způsobem se vytvářejí jako text v RTF formátu, třeba v programu Word, a z něho se překládají na soubor *.HLP. Existují i specializované programy dovolující interaktivní tvorbu. Pomocí WinHelp se pak zobrazí help soubor otevřený na vybrané stránce. Pipe - potrubí Pokud si aplikace vyměňují proud dat, lze je propojit prostřednictvím pipe, přenosového potrubí, která může být jednosměrné nebo obousměrné. Pipe se hodně podobají sériovému propojení počítačů, do jedné strany pouštíme data a z druhé strany vycházejí ven. Pipe samozřejmě nabízejí širší možnosti než sériová linka. Grafika Skriptum vynechalo skoro veškeré nabízené možnosti, především 3D grafiku Windows NT. Chyběly v něm i zmínky o podpoře multimédiích. A další prvky Témat, na něž nevybylo místo, zůstalo hodně; třeba sítě, pro něž Win32 nabízejí podporu, avšak ty však leží už velmi daleko od zaměření předmětu, podobně jako databáze, které se sice často používají v řízení, avšak jejich obsluha příliš závisí na konkrétní programovacím prostředí. Zde se s Vámi loučím a při tvorbě aplikací Vám přeji stabilní Windows a rychlé odhalení všech záhad Richard Šusta
215
10.2 Literatura Literatura v češtině Brandejs, M.: - Mikroprocesory INTEL, Grada, Praha 1991, ISBN 80-85424-27-4 (246 stran. Stručné informace o hardwaru procesorů 8086 a6 80486 a přehled jejich strojového kódu, avšak bez podrobnějšího popisu instrukcí.) Brandejs, M.: - Mikroprocesory Intel Pentium a spol., Grada, ISBN 80-7169-041-4 (416 stran.,Vydání předchozí knihy rozšířené o procesory Pentium.) Co v manuálu nenajdete - Příručka jazyka C++ pro začátečníky i profesionály, Kolektiv autorů (Kašpárek F., Minárik M., Nikolov V., Pecinovský R., Virius M.), UNIS 1993, ISBN v knize neuvedeno (762 stran, velmi podrobný výklad vhodný i pro neznalé jazyka C. Kniha se zaměřuje na překladače Borland C++, bohužel jen do jejich verze 3.1 [tj. pouze MS-DOS C++], takže chybějí novější C++ rozšíření svázaná s 32 bitovými programy. Mimořádná pozornost [skoro 200 stran] se věnuje výkladu proudů, o nichž byla ve skriptu pouze letmá zmínka v kapitole 3.113.11e na straně 90.) Herout: Učebnice jazyka C I. a II., České Budějovice, KOPP 1993. (Dva díly 272 a 240 stran. Přehledně zpracovaná učebnice, která nepředpokládá žádnou znalost jazyka C.) Pecinovský, R. - Virius, M.: Objektové programování 1 a 2, Učebnice s příklady v Turbo Pascalu a Borland, Grada, ISBN 80-7169-366-9 a ISBN 80-7169-436-3 (Dva díly, 232 a 264 stran. Přepracovaná knižní podoba výukového kurzu, který vycházel v Computer World. Souběžný výklad objektového Pascalu a C++. Určeno pro laiky.) Programování ve Windows 95, kolektiv autorů, Computer Press 1996, ISBN 80-85896-52-4 (610 stran, jedná se o výběr článků z Microsoft System Journal obsahující referenční poznámky ke tvorbě aplikací. Kniha není naprosto vhodná ke studiu programování Windows 95 a snad mohla by pomoci těm profesionálním programátorům, kteří nečtou Microsoft System Journal.) Racek, S. - Kvoch, M. - Třídy a objekty v C++, Kopp 1996 (219 stran, kniha předpokládá znalost jazyka C a vysvětluje objektové programování v rozsahu zhruba odpovídajícímu tomuto skriptu.) Richta, K. - Brůha,I.: Programovací jazyk C. [Skripta FEL], Praha 1991,1992,1993. (Doporučená publikace pro studium základů jazyka C. Shrnuje přednášky se stejnojmenného předmětu katedry počítačů a obsahuje vědomosti nutné pro zapsání předmětu Programovací jazyky pro řízení.) Stroustrup, B.: C++ Programovací jazyk, SA&S a BEN 1997 Praha, ISBN 80-86056-20-1 (686 stran. Spíš průvodce jazykem a organizací programu. Pouze 354 stran knihy [52%] se věnuje výkladu jazyka C++. Zbytek obsahuje: 120 stran diskuse nad návrhem programu a knihoven; 180 stran popisu referenční ANSI C++ normy, což má význam snad jen pro vývojáře C++ překladačů, a možná ani pro ně ne, protože se referuje ke stavu jazyka v roce 1991, tj. před celosvětovým přechodem na 32 bitové programy; zbylých 22 stran pak tvoří index a přehled knihy. Oproti tomu skriptu se v knize navíc probírají pouze datové proudy; dále se poněkud podrobněji hovoří o vícenásobné dědičnosti a o templates [šablonách].)
216
Literatura v angličtině Andrews, M.: C++ Windows NT Programming, M&T Books, 1994, ISBN 1-55828-300-5 (576 stran. Kniha podává informace o některých zvláštnostech 32 bitových systémů. Důraz se klade především na procesy, vlákna, pipes, sítě a dynamické knihovny. Uvádí sice málo témat, ale ty se probírají podrobně. Výklad specializovaný na uživatele Visual C++.) Borland ObjectWindows for C++, Borland International, Inc. 1991,1993, (420 stran. Velmi dobře udělaný výukový kurz OWL, který však předpokládá znalost objektů. Existuje i jako HTML text, který lze získat na serveru Borland; jeho kopie se nachází na WEB stránce skripta, URL viz. kapitola 0.) King, A.: Inside Windows 95 , Microsoft Press 1994, ISBN 1-55615-626-1 (478 stran. Obecné pojednání o struktuře Windows 95 bez výkladu jejich programování. Obsah se blíží víc populárně naučné publikaci.) Petzold, Ch.: Programming Windows 3.1, Microsoft Press, 1992 (983 stran. Klasická dobrá učebnice Windows, ale zaměřená pouze na Windows 3.1 API) Programming Windows 95 Unleashed, Sams Publishing 1995, ISBN 0-672-30602-6 (1084 stran zaměřených na programátory v MFC Visual C++. Kniha pojednává o¨pouze přínosech Windows 95 oproti Windows 3.1. Každou pasáž napsal jiný autor; těch se na knize podílelo celkem 20. Jednotlivé výklady na sebe nenavazují a mnohé věci se opakují, a naopak jiná fakta zase zcela chybějí. Uveden rozsáhlý, ale neucelený popis OLE 2.) Reinsdorf, K. - Henderson, K.: Teach Yourself Borland C++ Builder in 14 Days, Sams Publishing, 1998,97, ISBN 0-672-31267-0, ISBN 0-672-31051-1 (530 - 560 stran podle verze. Součást dokumentace k C++ Builder. Velmi dobře zpracovaný výukový kurz pro začátečníky předpokládající pouze základní znalost jazyka C. Nejde do příliš velké hloubky a uvádí jen minimální vědomosti. Druhá polovina knihy se věnuje použití komponent C++ Builder.) Reinsdorf, K. a Henderson, K.: Teach Yourself Borland C++ Builder in 21 Days, Sams Publishing, 1998,97, ISBN 0-672-31266-2, ISBN 0-672-31020-1 (800 stran. Samostatně prodávaná kniha, rozšířená oproti předchozímu titulu.) Sedgewick R.: Algorithms in C++, Princeton University 1992, ISBN 0-201-51059-6 (656 stran. Popis některých algoritmů řešených objektově, které se týkají oblasti stromů, třídění, hledání, zpracování řetězců, geometrie, grafů, statistiky, rychlé Fourierovy transformace, dynamického a lineárního programování. Uvádějí se pouze základní verze algoritmů a částečně jejich matematické pozadí.) User's Guide a Programmer's Guide, Borland International, Inc. 1991-5, (Kolem 414 a 420 stran podle verze překladače. Jde o manuály dodávané k překladačům firmy Borland. Velmi slušné popisy jazyka C a C++, i když místy až příliš podrobné, neboť autoři mají tendenci uvádět vyčerpávajícím způsobem všechny možné odlišnosti a záludnosti, a proto se v textu musí občas přeskakovat. Nicméně výklad se charakterem blíží učebnici a lze ho jednoznačně doporučit ke studiu. User's Guide je určený pro začátečníky, zatímco Programmer's Guide se zaměřuje na zkušenější programátory. )
217
10.3 Rejstřík pojmů
_ __except, 189 _cdecl, 25 _fastcall, 25 _pascal, 25 _stdcall, 25
A ActiveX, 215 adresa far, 29 fyzická, 27 lineární, 27 logická, 27 near, 29 reálná, 27 virtuální, 27 akcelerátory, 123 ANSI C, 10 API. viz Windows API aplikace Console, 113 GUI, 113 instance, 113 aplikace (application), 101 ASMP, 102 AssignMenu, 138 AT&T, 10
B base class, 71 Beep, 164 BeginPaint, 133 Bill Gates, 103 binder, 118 BIOS, 200 BOOL, 106 BOOLEAN, 106 BYTE, 106
C CALLBACK, 107 callback funkce okna, 128 catch, 187 CDECL, 106 CFrameWnd, 150 class, 59 class library, 98 clipping, 133 COLORREF, 106 COM, 215 COM1, COM2, 180 committed, 192 compile-time, 16 Console. viz aplikace, Console
control, prvky, 154 CreateDialog, 157 CreateFile, 178 CreateFileMapping, 196 CreateThread, 183 CWinApp, 150
D datový typ, 11 DC, 132 DDE, 214 deadlock, 112; 155 DEF, 135; 208 DEFINE_RESPONSE_TABLE1, 141 definice, 18 deklarace, 18 DESCRIPTION, 135 Destroy, 142 DestroyDialog, 157 DestroyWindow, 142 destruktor, 56 device context. viz DC dialog, 155 DialogBox, 157 DispatchMessage, 127 DllEntryPoint, 205 DR-DOS, 104 driver, 101 duration, trvání, 16 DWORD, 106 dynamická knihovna, 202
E Ellipse, 146 END_RESPONSE_TABLE, 141 EndDialog, 157 EndPaint, 133 EV_COMMAND, 141; 142 EV_WM_PAINT, 141 EV_WM_SIZE, 141 event driven, 114 EvPaint, 131; 146 exception. viz výjimky export, 21 EXPORTS, 209 extern, 23 extern, klíèové slovo, 18
F FAR, 105 FlashWindow, 164 friend, 83 fronta, 111
218
G GDI, 132 GetApplication, 145 GetDlgCtrlID, 159 GetDlgItemText, 159 GetExceptionCode, 190 GetExceptionInformation, 190 GetFocus, 161 GetMainWindow, 145 GetMenu, 145 GetMenuDescr, 145 GetMessage, 112; 127 GetNextDlgGroupItem, 161 GetNextDlgTabItem, 161 GetProcAddress, 202 GetStockObject, 128 GetSystemInfo, 191 GetWindow, 108 GetWindowDC, 133 GlobalAlloc, 206 GlobalMemoryStatus, 191 globální, 11 GNU, 104 goto, 13 Graphics Device Interface. viz GDI GUI. viz aplikace, GUI
H handle, 107 HBITMAP, 108 HCURSOR, 108 heap, 33 HFONT, 108 HGLOBAL, 108 HIBYTE, 106 HICON, 108 HIWORD, 106 HPEN, 108 HWND, 108
C CheckDlgButton, 159 CheckRadioButton, 159 childern, 108
I IBM, 103 IBM-PC, 103 Import Librarian, 117 INI soubor, 180 inicializace, 18 InitInstance, 150 InitMainWindow, 137 inline funkce, 18; 22
metoda, 63 pragma, 63 InSendMessage, 112 instance. viz aplikace, instance INT, 106 interrupt. viz pøerušení InvalidateRectange, 133 InvalidateRectangle, 128; 131
K Kernigham Brian, 10; 102 klávesnice, 180 konstruktor copy, 57 definice, 56
L LibMain, 213 ligh-weight-process. viz vlákno linkage, 23 linker, 21 LINUX, 104 LoadCursor, 108; 128 LoadIcon, 128 LoadLibrary, 202; 210 LOBYTE, 106 LocalAlloc, 206 lokální, 11 LONG, 106 LOWORD, 106 LPARAM, 106; 128 LPSTR, 106 LPVOID, 106 LRESULT, 106 lstrlen, 134
M m_pMainWnd, 150 MAKELONG, 106 MapViewOfFile, 197 memset, 41 menu, 123 MessageBeep, 165 MessageBox, 163 MessageBoxEx, 164 metoda inline, 63 virtuální, 92 MF_BYCOMMAND, 131 MF_BYPOSITION, 131 MF_CHECKED, 131 MF_UNCHECKED, 131 MFC, 8; 149 Microsoft, 103 modální, dialog, 155 model flat, 35 large, 34 modul, 117 Module Definition, 135 MS-DOS, 200 multi-threaded, 102
N NAME, 135 name mangling, 26 name space, 12 notification, 155; 162 notification code, 129
prototyp funkce, 22 proud, 90 pøerušení (interrupt), 101 pøetì ování, 66
Q queued messages, 111
O OBJ, 21 objekt dìdìní, 70 doèasný, 67 temporary. viz doèasný OCX, 215 offset, 28 OLE2, 214 ON_COMMAND, 150 OpenGL, 176 operátor (type), 37 [], 88 delete, 43 delete [], 45 ellipsis, 188 new, 43; 195 new [], 44 scope, 14 sizeof, 38 OS/2, 104 overloading, 66 OWL, 8; 136; 149 OwlMain, 136
P parent, 108 PASCAL, 106 PeekMessage, 112 pipe, 215 pointer far, 36 huge, 36 na funkci, 42 near, 36 PostMessage, 112 PostQuitMessage, 114; 129 preemptivní (pre-emptive), 102 proces (process), 101 procesor 80286, 30 80386, 31 8086, 28 mód chránìný, 31 reálný, 31 virtuální 8086, 31 registr FS a GS, 31 GDTR, 31 LDTR, 31 segmentu, 28 selektoru, 31 protected mode, 31
219
R RAD, 8 radio button, 160 reference, 46 registr systému, 181 RegOpenKeyEx, 181 reserved, 192 resource, 117 RGB, 106; 133 Richtie Dennis, 10; 103 RPL, 31 Run, 136 run-time, 16
S scope, 11 segment, 28 SendDlgItemMessage, 159 SendMessage, 112; 158 SetBkColor, 135 SetBkMode, 135 SetCursor, 108 SetDlgItemInt, 159 SetDlgItemText, 159 SetIcon, 138 SetMainWindow, 138 SetThreadPriority, 184 SHORT, 106 ShowWindow, 125 sizeof. viz operátor sizeof Sleep, 184 SMP, 102 stack, 33 static, 16; 23; 85 storage class, 11 stream. viz proud STRICT, 108 struct, 22; 59 STUB, 120; 135 STUB, 118
S šablona. viz template
T tag, 13; 23 TApplication, 136 task modal, 155 TBrush, 146 TColor, 146 TCommandEnabler, 147
TDialog, 166 TDSTR32.EXE, 120 TDSTRIP.EXE, 120 template, 97 TerminateThread, 184 TextOut, 146 TFrameWindow, 137 this, 54 thread. viz vlákno throw, 187 TI, 31 time slices, 101 TLB, 32 TLS, 206 TModule, 138 topmost, 110 TPen, 146 TranslateAccelerator, 127 TranslateMessage, 127 TRect, 146 TResId, 138; 150 try, 187 TSR, 101 TWindow, 139 TWindowAttr, 138
U
W
událostmi øízený (event driven), 102 UINT, 106 úloha (task), 101 ULONG, 106 Unicode, 176 UNIX, 10; 102 USHORT, 106
V VCL, 8; 168 víceprocesní (multitask), 101 víceprocesorový (multiprocessor), 102 víceúlohový. viz víceprocesní víceu ivatelský (multiuser), 102 viditelnost, 13 virtual, 93 VirtualAlloc, 192 visibility. viz viditelnost vlákno, 206 vlákno (thread), 101; 182 VOID, 105 výjimky C++, 187 strukturované, 189
220
WEP, 213 Win16, 6 Win32, 6; 175 WINAPI, 106; 107 Windows, 104 Windows 95,98 a NT, 175 Windows API, 121; 122 WinMain, 113; 114 WM_COMMAND, 129; 155 WM_CREATE, 128 WM_DESTROY, 129 WM_CHAR, 112 WM_PAINT, 116; 125; 129; 133 WM_QUIT, 112; 114; 129 WM_SETTEXT, 110; 154 WM_SHOWWINDOW, 128 WORD, 106 WPARAM, 106; 128 WritePrivateProfileString, 181 WS_GROUP, 159 WS_OVERLAPPEDWINDOW, 127 WS_TABSTOP, 160
X X-Windows, 104