Objektově orientované programování v jazyku C++
Ing. David Procházka, Ph.D., Ing. Ondřej Popelka, Ph.D., Mgr. Tomáš Foltýnek, Ph.D., Ing. Jan Kryštof, Ph.D., Ing. Hana Netrefová, Ph.D.
2
Obsah
Obsah Cíl kurzu
3
Návaznost
3
Motivace
3
Poučení
3
Úvod
3
1 Základy jazyka C++ Historie a vývoj jazyka C++ Překlad a základy jazyka . Řídící struktury . . . . . . . Funkce a rekurze . . . . . . Vstupní a výstupní proudy
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
5 5 6 9 13 16
2 Objektově orientované programování Základní pojmy a koncepty OOP . . . . . . . . . . . Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstruktor, destruktor . . . . . . . . . . . . . . . . . Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Vztahy . . . . . . . . . . . . . . . . . . . . . . . . Asociace . . . . . . . . . . . . . . . . . . . . . . . Test . . . . . . . . . . . . . . . . . . . . . . . . . . Agregace . . . . . . . . . . . . . . . . . . . . . . . Dědičnost . . . . . . . . . . . . . . . . . . . . . . Test . . . . . . . . . . . . . . . . . . . . . . . . . . Příklady . . . . . . . . . . . . . . . . . . . . . . . 2.2 Pokročilé techniky OON . . . . . . . . . . . . . Složitější případy dědičnosti a polymorfismus Test . . . . . . . . . . . . . . . . . . . . . . . . . . Návrhové vzory . . . . . . . . . . . . . . . . . . . Příklady . . . . . . . . . . . . . . . . . . . . . . . Souhrn . . . . . . . . . . . . . . . . . . . . . . . . . . . Příklady . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
19 19 1 2 1 4 4 1 2 10 1 3 9 9 1 3 7 10 10
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
3 Práce s pokročilými vývojovými nástroji Co v této kapitole naleznete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Animace: Visual Paradigm – vytvoření diagramu tříd a základní třídy . . . . . . . . . Animace: Visual Paradigm – tvorba a nastavení základním vazeb v diagramu tříd . . Animace: Visual Paradigm – generování kódu z diagramu tříd a rekonstrukce diagramu tříd z kódu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22 22 22 22
4 Práce s řetězci, soubory a výjimky Soubory pomocí proudů . . . . . Práce s řetězci – třída String . . . Výjimky . . . . . . . . . . . . . . . Příklady . . . . . . . . . . . . . . .
22
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
23 23 27 29 33
5 Knihovna STL a vybrané nástroje OOP Datové kontejnery . . . . . . . . . . . . . Algoritmy . . . . . . . . . . . . . . . . . . Vybrané nástroje OOP . . . . . . . . . . . Příklady . . . . . . . . . . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
44 44 48 54 63
6 Grafické knihovny OpenGL, DirectX, XNA a ti další... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OpenGL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Příklady . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
69 69 70 74
. . . .
. . . .
. . . .
3
Obsah Závěr
75
Slovníček pojmů
76
4
Obsah
Cíl kurzu Cílem tohoto eLearningového kurzu je seznámit studenty se základními myšlenkami objektově orientovaného návrhu a jejich implementací převážně v jazyku C++. Po absolvování kurzu by měl být student schopen převést určitý programátorský problém do podoby objektového modelu a ten umět zapsat v jazyku C++. Důraz bude kladen na využívání knihoven C++ jakými jsou STL, aj.
Návaznost Tento kurz nemá žádné přímé prerekvizity, je však přínosné, pokud se čtenář orientuje v základech programování v jakémkoliv jazyku.
Motivace Motto tohoto kurzu jsem vybral z knihy o objektově orientovaném návrhu pana Boocha: Hvězda v kolapsu, dítě učící se číst, buňka bílé krvinky útočící na virus, jsou tři objekty světa. Každý z nich je děsivě složitý. Einstein tvrdil, že každé vysvětlení přírody musí být jednoduché, protože Bůh není ani ješitný ani panovačný. Programátoři však nejsou tak důvěryhodní. Vystihuje hlavní myšlenku kurzu. Vytváření programů je velmi složitá činnost, která leží na pomezí vědy a umění. Metodika tvorby programů prochází neustále evolucí. Aktuálním trendem je objektově orientované programování a z něj vycházející techniky. Důvodů pro studium OOP je celá řada. Jen namátkou: • Vychází z něj mnoho dalších metodik - návrhové vzory, programování řízené testy, atp. • Většina rozsáhlejších programátorských projektů je založena právě na této metodice. • Programovací jazyky, které jsou dnes nejprogresivnější - Java, C#, C++, PHP, Python jsou čistě objektové nebo objekty přímo podporují. Pro studenty informatických oborů je proto nezbytné osvojit si základy práce v této oblasti. Cílem tohoto kurzu je seznámit Vás se základy této metodiky.
Poučení Programování se nelze naučit čtením, jen zase programováním. Chápejte tuto oporu jako návod, jaké problémy si máte prakticky vyzkoušet. Každou část knihy si po přečtení vyzkoušejte, abyste si problém skutečně osvojili. Čtěte pozorně! Pokud Vám nebude něco jasné nebo naleznete chybu, prosím, napište nám. Opora je stále ve vývoji. Snažíme se ji postupně doplňovat o výstižnější příklady a eliminovat typografické zrůdnosti, kterých se díky eLearningovému prostředí (a našim chybám) dopouštíme.
Úvod Vývoj programů Problémy při vývoji programů V okamžiku, kdy začnete tvořit rozsáhlejší programy, narazíte na řadu problémů. • Složitost řešeného zadání (komplexní systémy, složitá funkčnost, • Problém vývoje (synchronizace pracovníků, změny v zadání, ...) • Program je stavový automat (Svět je spojitý systém - na malou změnu na vstupu dostanu malou změnu na výstupu. Program se chová diskrétně - mění stavy skokově. ) Důsledky těchto problémů budou chyby v softwaru, zpoždění projektů, nespokojenost zákazníků atp. Velké části těchto problémů se dá předejít zvolením vhodné metodiky vývoje. Nad vývojem programu se dá uvažovat ve dvou rovinách. Prvním krokem je analýza problému a jeho pochopení. Teprve potom se aplikuje postup podle zvolené metodiky.
5
Obsah
Řešení problémů V průběhu analýzy zadání, i při jeho implementaci, používáme pro zjednodušení naší práce obvykle tři nástroje: • Dekompozici: Rozdělení problému na menší části a jejich postupné řešení. • Abstrakci: Ignorování nedůležitých detailů. Vytváření zjednodušených modelů. • Hierarchii: Nalézt hierarchické závislosti v systému není vždy jednoduché. Umožní však lépe popsat vzájemné vazby mezi částmi systému, a tím ho výrazně zjednodušit. Jakkoliv se tyto pojmy mohou zdát v tomto okamžiku poněkud abstraktní, bez přímé vazby na programátorskou praxi, ukáže se, že většina postupů používaných při objektově orientovaném programování jsou aplikacemi těchto obecných myšlenek.
Velmi krátká historie vývoje programovacích technik • Nestrukturované programování (jeden kontinuální program, globální data) • Procedurální programování (části kódu jsou uloženy do procedur, kontrola funkce jednot• • • •
livých procedur - menší chybovost, jeho rysy jsou de facto používány při implementaci metod tříd) Modulární programování (Program je rozdělen na více malých částí, které spolu komunikují pomocí volání procedur. Každý modul má svoje data.) Objektově orientované programování (Program je rozdělen na objekty. Každý objekt obsahuje jak data, tak procedury na jejich obsluhu.) Metody vycházející z OOP (Tím se dostáváme do současnosti. Patří sem zejména techniky jako jsou programování řízené testy, programování řízené modely, extrémní programování a další.) ”Post-objektové” techniky (Toto označení je velmi diskutabilní, ale lze se např. zařadit SOA (Service Oriented Architecture), která uvažuje, podle jedné z definic, jako základní kámen programu nikoli třídu, ale skupinu tříd, které vykonávají společně určitou funkci – službu.)
Vedení projektu založeného na OOP Objektově orientované programování, které je předmětem tohoto tutoriálu, se skládá z o. o. analýzy a o. o. návrhu. Obecné fáze projektu (částečně podle Bruce Eckela) jsou následující: 1. Příprava plánu (vrhnu se na to vs. příprava) 2. Základní téma (loď připluje, naloží, odpluje) 3. Co budeme vytvářet (Use Case Diagram) 4. Jak budeme systém budovat (Class-Responsibility-Collaboration karty) 5. Vytvoření jádra systému 6. Iterace případů použití a evoluce Existuje samozřejmě mnoho různých metodik vedení projektů, jejich studium by si dokonce zasloužilo samostatný kurz. Tyto body jsou ale poměrně univerzální a velká část různých metod vedení projektů tím či oním způsobem rozvíjí tuto základní myšlenku. Chtěl jsem tím v zásadě upozornit na fakt, že obvykle je doporučováno postupovat při vývoji softwaru trochu jiným, uvážlivějším, způsobem, než ”hrrrr na to, ať je to za námi”.
6
Základy jazyka C++
1
Základy jazyka C++
Historie a vývoj jazyka C++
Historie jazyků rodiny C Jazyk C Historie C++, který je v tomto kurzu používán se odvíjí od jazyku C. Kolem roku 1970 byl tým programátorů kolem Dennise Ritchieho v Bellových laboratořích pověřen vývojem nového operačního systému pro telekomunikační AT&T. Tím systémem se měl stát Unix. Programátoři ale naznaly, že nemají k dispozici programovací jazyk, který by odpovídal jejich potřebám, proto se rozhodly napsat si vlastní jazyk - C. Název C má poměrně prozaický původ. V té době existoval jazyk B a proto padla volba na C - následující volné písmeno. Podrobnosti o vývoji jazyka můžete najít přímo na stránkách Bellových laboratoří 1 . V průběhu doby se jazyk C silně měnil. Jeho evoluci lze ve stručnosti popsat takto:
• 1978: Brian Kernighan a Dennis Ritchie vydávají The C Programming Language. Tato kniha určila první neoficiální standard pro C označovaný jako C podle Kernighama a Ritchieho (K-R C). • ANSI C: Existuje několik verzí normy, nejrozšířenější verze je z r. 1989. • ISO/IEC C (ISO 99): Poslední verze normy – ISO/IEC 9899:1999. V roce 2001 byly vydány opravy. Jazyk C lze popsat jako:
• • • • •
relativně nízkoúrovňový (systémový) jazyk, ideální pro vývoj operačních systémů, driverů, překladačů, podporuje obrovské množství knihoven, nižší efektivita vývoje, která je vykoupena rychlým a elegantním kódem, podporuje objekty – Objective C (Na vývoji Objective C se podílí velkou mírou firma Apple. Koncepce objektů je silně odlišná od ostatních jazyků rodiny C. V současné době je k dispozici nová verze jazyka – Objective C 2).
Jazyk C++ Tímto se pomalu dostáváme k jazyku C++. Začněme jeho názvem - C++. Je evidentní, že vychází z názvu svého předchůdce - C. ”++” je v C operátor pro inkrementaci nebo následníka. C++ tedy znamená ”následník C”. C++ je jazyk od počátku orientovaný na objektový návrh. Vznik C++ můžeme datovat přibližně do roku 1985 a opět do Bellových laboratoří. Jeho autorem je Bjarne Stroustrup. C++ se stal vzorem pro implementaci mnoha jiných objektových jazyků - C#, Java, aj. Jako správný následník si C++ snaží zachovat kompatibilitu s C. Jakýkoliv program v C by měl být platným programem v C++ a měl by jít přeložit překladačem pro C++. První překladače C++ byly preprocesory, které překládaly z C++ do C. Dnes již některé programy v jazyku C překladači pro C++ překládat nelze, ale zpětná kompatibilita s C je pořád velmi dobrá. Pokud je program napsán podle současných norem C, je z 99% přeložitelný překladačem C++. Tohoto jevu je v praxi stále hojně využíváno, protože nové moduly mnoha programů v C chtějí využívat např. STL, objekty, či jiné nástroje, které poskytuje pouze C++. C++ je v současné době samořejmě také standardizován - viz popis standardů 2 . Základní vlastnosti C++:
• • • • • •
1 2
jazyk od počátku orientovaný na OOP, rychlejší vývoj aplikací oproti C, abstraktní datové typy, výjimky, odlišná filozofie práce se soubory, terminálem, atp. jakkoliv je zpětně podporována většina konstrukcí C, není dobré plést dohromady ”C” a ”C++” kód.
http://cm.bell-labs.com/cm/cs/who/dmr/chist.html http://open-std.org/jtc1/sc22/wg21/
Základy jazyka C++
Jazyk C# C# (sí šarp) označuje hudební předznamenání, které zvyšuje notu o půl tónu. C# tedy v hudební nauce značí ”cis” – zvýšené C. C# je vysoko úrovňový objektově orientovaný jazyk vyvinutý firmou Microsoft (Andersem Hejlsbergem) pro platformu .NET. Byl schválen standardizačními komisemi ECMAa ISO. Založen na jazycích C++ a Java. Jedná se prakticky o přímého konkurenta Javy, ze kterého silně čerpá (na druhou stranu v současné době Java také čerpá ze C#). C# se využívá hlavně k tvorbě databázových programů, webových aplikací, webových služeb, ale i desktopových aplikací. Původně existoval pouze oficiální překladač firmy Ms pro platformu Windows, ale dnes je již k dispozici projekt Mono 3 , který je otevřenou multiplatformní implementací překladače (interpreta) pro C# a další jazyky .NET frameworku. Projekt je pod patronací firmy Novell. Zásadními rysy C# jsou: • jazyk C# může být transformován jak do strojového kódu, tak do Common Intermediate Language 4 , resp. Common Language Runtime 5 (v současné době ale překladač do strojového kódu neznám), • automatický garbage collector, • orientace na rychlost vývoje, zvláště webových aplikací, • neexistují globální proměnné, • ukazatele mohou být použity pouze v blocích kódu explicitně označených jako ”unsafe”, • silně typový jazyk (hlídá si konverze mezi typy ještě více, než C++), • nevýhodou je, že oficiálně je podporován pouze na platformě Ms Windows.
Překlad a základy jazyka
Překlad programů v C++ a základy syntaxe Překlad pomocí GNU C++ překladače Způsob překladu se liší podle překladače, který je použit a případně podle vývojového prostředí. Existuje řada vývojových prostředí, ve kterých lze překlad realizovat prakticky zmáčnutím tlačítka. My budeme v rámci této opory pracovat překladačem GNU C++. Byl zvolen z toho důvodu, že je zdarma a že je dostupný prakticky pro všechny platformy. Přesto je však nutné zdůraznit, že GNU C++ není jedinou nebo obecně nejlepší volnou. Velmi kvalitní je např. překladač firmy Microsoft, který je poměrně rychlý a produkuje dobře optimalizovaný kód. Naučme se ale nyní základním příkazům umožňujícím překlad kódu z příkazové řádky. (Jako ideální prostředí pro zkoušení programování v C++ doporučuji UNIXové systémy, které mají dobrou ochranu paměti a velmi dobře si rozumí s terminálem, který budeme pro překlad a ladění používat.) Základní příkaz pro překlad kódu v GNU C++ je: c++ zdrojovy kod.cc -o binarni soubor
"c++” je samozřejmě příkaz pro překlad (ekvivalentem je příkaz g++). "-o" je parametr určující název výstupního souboru. V případě platformy Windows je to obvykle ”jmeno souboru.exe”, v UNIXových systémech je to prostě ”jméno souboru”. Soubor se zdrojovým kódem je důrazně doporučeno pojmenovávat s příponou ”cc”, případně ”cpp”. Další informace naleznete na stránkách překladače nebo manuálových stránkách.
Základní informace
Jazyk C++ rozlišuje malá a velká písmena. ”MojePromenna” je tedy něco zcela jiného, než ”mojePromenna”.
3
http://www.mono-project.com/ http://en.wikipedia.org/wiki/Common Intermediate Language 5 http://en.wikipedia.org/wiki/Common Language Runtime 4
7
8
Základy jazyka C++ Komentování Komentování kódu, aby jej překladač nebral v potaz, lze dělat pomocí dvou lomítek ”//” nebo pomocí dvojce ”/*” a ”*/”. Je důrazně doporučeno používat pro komentování jednoho, dvou řádků pouze ”//” a ne druhý způsob. Připravili byste se tak o možnost zakomentovat pomocí nich větší část kódu.
// takto zakomentuji vetsi cast kodu /* int promenna, operand1, operand2; operand1 = 10; // sem jsem pridal komentar //operand2 = 20; // tento radek jsem zakomentoval operand2 = 30; /* pokud sem napisu tento komentar, tak "velky komentar" skonci zde a ne az dole */ promenna = operand1 + operand2; / // zde skoncil velky komentar
Hello world
/* nasledujici program vypise na obrazovku napis "Ahoj svete" */ #include
// nacteni knihovny pro vypisovani na terminal using namespace std; // urceni pracovniho jmenneho prostoru // hlavni funkce programu, ktera se automaticky zavola jako prvni int main(){ // vypis textu do terminalu, prozatim tise akceptujme cout << "Ahoj svete" << endl;
}
Datové typy a proměnné
• Základní – Celočíselné: int, longint, short int – Reálné: float, double, long double – Znaky a řetězce: char (znak) a string (řetězec) – Prázdný tip: void
• Speciální – pole, – výčtový typ, – záznam, union, – seznam, fronta, zásobník, ... (podporováno prostřednicvím STL) – třída, struktura (Zařazení třídy mezi datové typy je samozřejmě diskutabilní, ale často je na místě běžně užívaných typů uváděna, proto jsem ji do výčtu také zařadil.) Rozsah typu závisí na architektuře a překladači, proto nemá smysl uvádět jejich velikosti nebo rozsahy. Zjistíme jej pomocí příkazu sizeof(int).
Deklarace a inicializace proměnné: typ jmeno = inicializacni hodnota
Základy jazyka C++
int i = 1; // globalni promenna int main(void) { int j; // lokalni promenna return 0;
}
Globalní proměnnoulze číst a modifikovat z jakéhokoliv místa programu. Lokální proměnná platí pouze v dané funkci nebo struktuře. Globální proměnné vytváříme mimo funkce a třídy. Pokud chceme do proměnné přiřadit hodnotu, použijeme znak ”=”, ne ”:=”, jak je tomu např. v Pascalu.
K vypsání určitého řetězce na obrazovku budeme používat následující konstrukci: cout << "Ahoj", k výpisu proměnné cout << nazevPromenne. Pokud budeme chtít skočit na nový řádek, ”pošleme” na cout endl. Výstupy lze spojovat: cout << "Hodnota je: " << vysledek << endl; Prozatím to berme jako fakt. Pricip práce s proudy bude vysvětlen dále.
Pole Pole trochu zvláštní, byť velmi používaný, typ. Zmíním se proto o něm trochu více. Pole může být obecně vícerozměrné a může obsahovat jakýkoliv typ a dokonce i objekty. Ukažme si problém na příkladu jednorozměrného a dvourozměrného pole celých čísel:
int pole1d[10]; // deklarace pole1d = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // naplnění hodnotami pole1d[5] = 11; // zápis na určitou pozici float pole2d[2][2]; //deklarace 2D pole pole2d[0][0] = 19.9119; //zápis na určitou pozici
Pole jsou vždy indexována od nuly!
Výčtový typ Je nejjednodužší možností, jak si definovat vlastní typ. Potřebujeme například ukládat barvy, které může mít karoserie auta:
enum Barvy {cervena, modra, bila, cerna}; int main(){ Barvy karoserie; karoserie = cerna; ...
}
9
10
Základy jazyka C++
O dalších strukturách typu záznam a union se nebudu zmiňovat. Jejich popis naleznete v běžně dostupné literatuře. Osobně však doporučuji zvážit jejich nahrazení pomocí šablon a objektů, o kterých bude psáno dále. Kód se tak stane přehlednějším a ucelenějším. Tyto struktury měly smysl hlavně v neobjektovém C, v C++ je jejich využívání diskutabilní.
Aritmetické výrazy Příkazy jsou v C++ zpravidla ukončeny středníkem. Výraz ukončený středníkem se stává příkazem.
• i = 2 – výraz s přiřzením • i = 2; – příkaz Operátory
• operátory binární: +, Γ, *, /, % c = a+b;
• opertory unární: ++, ΓΓ j = 5; j++; // zvedni hodnotu j o 1 i = ++j; // uloz do i hodnotu j zvedlou o jedna • operátory přiřazení: =, +=, Γ=, *=, /=, %= i += a // ekvivalentní výrazu i = i+a; • operátory pro booleovské výrazy: ==, ! =, &&, ——, ! if (a==b) ... if (!a == true) ... • relační operátory: <, >, <=, >= if (a>b) ...
Napište program, který v hlavní funkci programu vytvoří proměnné ”a” a ”b”. Uložte do nich dvě libovolná čísla. Postupně vypisuje na obrazovku výsledky jejich součtu, násobení, inkrementace, atp. Pro výpis použijte příkaz z ”Hello world” příkladu. Např.: cout << "Soucet je: " << a+b << endl;
Řídící struktury
Řídící struktury v C++ Smysl řídících struktur je v tom, že umožňují řízení běhu programu. Program díky nim nemusí běžet řádek po řádku, ale může některé části kódu přeskakovat, opakovat, atp. Pracovně můžeme řídící struktury rozdělit na cykly a podmínky. Začněme podmínkami.
Podmínky Jak bylo řečeno, smysl podmínek je v tom, že umožňují přeskakovat určité části kódu, resp. umožňují vybrat, která z několika částí kódu se provede. Základní podmínkou je dvojce příkazů: if - else Princip si vysvětlíme na jednoduchém příkladu.
Základy jazyka C++
int proved = 1; int hodnota = 5; if (proved == 1){ hodnota = 10;
}
Program funguje následovně: Pokud je v proměnné proved uložena jednička, bude na konci kódu v proměnné hodnota 10. Pokud bude v proved jiné číslo, kód ve složených závorkách se neprovede a v hodnota bude 5. Pokud je v těle podmínky pouze jeden příkaz, jako v předchozím příkladu, lze ji zapsat bez složených závorek – if (proved == 1) hodnota = 10; Příkaz if můžeme rozšířit použitím else. Else může být opět podmíněno nějakou hodnotou nebo nepodmíněné (tj. provede se vždy, pokud se neprovede kód u if). Použití bude patrné z následucícího příkladu:
if (volba == 1) { cout << "Uzivatel vybral volbu 1" << endl; cout << "Soucet je: " << a+b << endl; } else if (volba == 2) { cout << "Uzivatel vybral volbu 2" << endl; cout << "Rozdil je: " << a-b << endl; } else { cout << "Uzivatel vybral jinou volbu, nez 1 nebo 2" << endl;
}
Pokud bude volba rovna jedničce, provede se první sekce kódu, pokud dvojce, provede se druhá, pokud čemukoliv jinému, provede se poslední sekce. Switch Pokud bych měl psát několik if podmínek za sebou, jako je tomu v předchozím příkladu, mohu je nahradit příkazem větvení – switch.
switch (volba) { case 1 : c = a+b; // zde jsem "úmyslně" zapomněl dát brake; bude se provádět i case 2 - častá chyba case 2 : c = a-b; d = c+b; break; case 3 : c = a*b; break; default : cout << "Neznama volba"; break;
}
Rozhodování, která část kódu se provede, se provádí na základě hodnoty uložené v proměnné volba. Podle ní se vybere čás kódu uvedená návěstím case s příslušnou hodnotou. Provádění příkazů pokračuje až do nalezení příkazu break, ne do dalšího návěstí case! V případě, že ve volbě bude uložena jednička se tedy provede jak příkaz se sčítáním a a b, tak příkazy patřící k návěstí 2. Za příkazem sčítání totiž není break. Ternální operátor Ternální operátor je specifikum jazyka C. Jedná se o zkrácený zápis jednoduché podmínky přiřazení. Ukažme si to opět na příkladu.
11
12
Základy jazyka C++
if (j == 1){ hodnota = 5; } else { hodnota = 10;
}
lze zapsat:
hodnota = (j == 1) ? 5 : 10; // Pokud je i rovno 1, pak do hodnota uloz 5, jinak 10.
Jaká hodnota bude uložena v i a k na konci toho kódu?
int i, k, j = 2; i = (j == 2) ? 1 : 3; k = (i > j) ? i : j;
Cykly Cykly rozlišeme na ty, které mají pevný počet opakování a na ty, jejichž počet opakování je dán platností určité podmínky. Cyklus s pevným počtem opakování je v C++ jeden - for (z určitého úhlu pohledu mezi tyto cykly patří i for each, tj. jsou dva).
For Počet opakování cyklu je dán podmínkou zapsannou v jeho hlavičce. Např. proměnná counter bude postupně nabývat hodnot od 0 do 9 a při každém průchodu cyklem se zvýší o jedna (viz následující příklad).
int counter; for (counter = 0; counter<10; counter++){ cout << counter << endl;
}
Proměnná counter má na počátku hodnotu nula a bude se pomocí unárního operátoru ++ zvětšovat o jedna, dokud bude platit podmínka uvedená v závorce na druhém místě. Současná verze C++ nám umožňuje deklarovat řídící proměnnou cyklu přímo v samotné deklaraci cyklu (závorce). Tento krok je velmi vhodný pro udržení přehledosti kódu. Proměnná totiž nebude platit v rámci celé funkce (metody, atp.), ale jen do dobu provádění cyklu. Tím můžeme zabránit zbytečnému nedorozumnění ”k čemu je tato proměnná?”.
Základy jazyka C++
int i = 1; i = a + b; // pouzivam promennou k vypoctum ... // zapomel jsem, ze jsem i pouzil k vypoctum for(i = 0; i<10; i++){ ...
} // po skonceni cyklu uz v i neni soucet, ale 9
Uděláme předchozí příklad trochu čistěji:
int i = 1; i = a + b; // pouzivam promennou k vypoctum ... // deklaruji novou promennou i pouze pro rizeni cyklu for(int i = 0; i<10; i++){ ...
} // po skonceni cyklu v puvodnim i je stale soucet // i, ktere pouzival cyklus uz neexistuje
Nezáleží na tom, zda budete počítadla v cyklech na počátku nastavovat na 0 nebo na 1. Je ale dobré zvyknout si na jeden způsob a ten dodržovat.
While a do-while Cyklus se provádí tak dlouho, dokud platí podmínka. Syntaxe:
while(podminka){ prikaz1; prikaz2; ...
} int x = 0; while(x<10){ x++;
}
V tomto případě jsme podmínku běhu uložili na začátek. Může nastat situace, že se cyklus neprovede vůbec. Pokud chceme, aby se cyklus vykonal vždy alespoň jednou, dáme podmínku na konec.
x = 0; do{ x++; } while(x!=1)
13
14
Základy jazyka C++
Jaké číslo bude uloženo v proměnné x na koncích kódu v předchozích dvou příkladech? Kolikrát cykly proběhnou?
For each C++ podporuje mimo jiné i cyklus for each, známý z jazyků Java nebo C#. Tento cyklus umí pracovat jak s jednoduchými poli, tak např. s iterátory a šablonami (bude o nich řeč později). Ukažme se příklad např. na vypsání prvků pole. Princip tohoto cyklu spočívá v tom, že v prvních dvou parametrech specifikujeme od kterého po který prvek se má pole procházet a v posledním parametru uvedeme jméno fukce (viz následující text), které obdrží jako parametr vždy aktuální prvek pole a může s ním něco provést.
#include using namespace std; void vypis(int prvek){ cout << "Aktualni prvek je: " << prvek << endl;
} int main() { int pole[5]; // ulozi postupne hodnoty 0,1,2... for(int i=0; i<5; i++){ pole[i] = i;
} // vypise prvky pole, u identifikace pozice musi byt uveden operator & // o nem bude rec pozdeji, prozatim pouze vemte na vedomi, ze tento cyklus existuje for each(&pole[0], &pole[5], vypis); return 0;
}
Funkce a rekurze
Procedury a funkce Funkce je nástroj, který umožňuje vyjmout část kódu z programu a ten podle potřeby opakovaně volat. Typickým příkladem je složitý výpočet, který opakovaně voláme na různých místech programu. V Pascalu a jiných jazycích se můžeme setkat s pojmem procedura. Označuje se tak funkce, která nevrací žádnou hodnotu (jen provede např. výpis na obrazovku nebo zmodifikuje určitou globální proměnnou). V C++ pojem procedura nepoužíváme. Bavíme se vždy pouze o funkcích. Pokud nemá funkce vracet žádnou smysluplnou hodnotu, vrací prázdný typ void (viz následující příklad).
void vypisCislo(int cislo){ cout << "Predane cislo je: " << cislo << endl; cout << "Naslednik cisla " << cislo << " je cislo " << cislo++ << endl;
}
Základy jazyka C++
Vracení hodnot funkcí Základním problémem je, jak vrátit hodnotu vypočtenou v těle funkce. Nastávají dvě situace: potřebujeme vrátit jen jednu hodnotu a potřebujeme vrátit více hodnot. V prvním případě využijeme klíčového slova return, které ukončí provádění procedury a vrátí určitou hodnotu.
int secti(int cislo1, int cislo2){ return cislo1+cislo2;
} int int int int int
main(){ x = 12; y = 45; a = secti(x,y); b = secti(10,12);
cout << b << endl; return 0;
}
Často však potřebuje vrátit více, než jednu hodnotu. Máme 2 možnosti, jak to provést. Jako subjektivně jasnější doporučuji následující:
void vymen(int &a, int &b){ int pom = a; a = b; b = pom;
} int main(void){ int a = 10; int b = 5; ... vymen(a, b); // ted je v a petka a v b desitka ...
}
Všimněte si znaku &, který je v hlavičce funkce před názvy proměnných. V praxi se proměnné předané do funkce chovají následovně: vytvoří se jejich lokální kopie, se kterou pracujeme po dobu běhu funkce a můžeme je modifikovat. Na konci funkce jsou ale veškeré modifikace zahozeny, protože jsou zrušeny lokální kopie, se kterými jsme pracovali. Znaménko & překladači říká, že nemá vytvářet jen lokální kopie, ale že změny mají zachovat a vrátit zpět do místa, odkud byla funkce zavolána.
Pole jako parametr funkce Trochu speciálním případem parametru je v C++ pole. Pole se totiž nepředává jako celek, ale předává se jen ukazatel na začátek pole. Pro korektní práci s polem tedy musíme předat do funkce jak ukazatel na začátek pole, tak jeho velikost. Ukažme si to na příkladu funkce, která vrátí součet prvků v poli.
15
16
Základy jazyka C++
// pole v parametru je ukazatel int sectiPole(int scitanePole[], int velikost){ int soucet = 0; for(int i=0; i
} return soucet;
} int main(void){ int pole[10]; for(int i=0; i<10; i++) pole[i] = i; // naplnim pole hodnotami cout << "Soucet: " << sectiPole(pole, 10) << endl; return 0;
}
Deklarace a definice funkce Občas je nutné použít (zavolat) určitou funkci ještě před tím, než je napsána. Případně máme několik funkcí, které se volají navzájem a je těžké je poskládat tak, aby byly ve vhodném pořadí. Problém lze řešit tak, že na počátek kódu uvedeme nejdříve deklarace funkcí (jen jejich hlavičky s počty a typy parametrů) a jejich definici (implementaci) napíšeme později.
#include using namespace std; // deklarace funkci int secti(int, int); double vypocti(int, int); double vypocti(int a, int b){ double mezivysledek = secti(a, b); mezivysledek = mezivysledek/a; return mezivysledek;
} int secti(int x, int y){ return x+y;
}
Je nutné ale dodat, že v případě, že máme v programu řadu vzájemně provázaných funkcí, je obvykle účelné, sáhnout po objektovém návrhu a roztřídit je do vhodných tříd. Tak totiž tento problém zcela odpadá.
Rekurze Rekurze samozřejmě značí volání sama sebe. V C++ je rekurze standardně podporována, proto jakákoliv funkce může volat samu sebe. Jediná funkce, kterou nelze rekurentně volat je hlavní funkce main. Je pochopitelně nutné vhodně nadefinovat zarážku rekurze, jinak bude volání probíhat do nekonečna.
Základy jazyka C++
int faktorial(int cislo){ if (cislo>0) return cislo * faktorial(cislo-1); else return 1;
} init main(){ cout << faktorial(cislo); ...
}
Vstupní a výstupní proudy
Vstup z terminálu a výstup na něj Práce s terminálem (často označovaným jako standardní vstup/výstup) se v C++ od C značně liší. Fungují samozřejmě veškeré příkazy, které jsou definovány v knihovne stdio.h, jejich využívání se však C++ příliš nedoporučuje. C++ zavádí techniku, která je označována jako proudy. Při práci s terminálem jsou definovány 2 základní proudy: znakový výstup – charout – cout a znakový vstup – char-in – cin. Abychom mohli tyto proudy využívat, musíme si načíst knihovnu iostream a protože příkazy na práci s terminálem spadají do jmeného prostoru std (o jmenných prostorech bude řeč dále), je vhodné napsat též deklaraci using namespace std (tedy používej příkazy ze jmeného prostoru std). Do proudu cout můžeme ”zasílat”, co se má na terminálu vypsat a z proudu cin můžeme přebírat, co zadá uživatel na klávesnici. Způsobů zasílání je mnoho, ale nejčastěji bývají využívány operátory >> a <<. Např.: pokud chci vypsat na standatdní výstup řetězec ”Ahoj světe”, využiji výstup cout a operátor <<.
#include using namespace std; int main(){ cout << "Ahoj" << endl; return 0;
}
Jak vidíte, operátor << nám umožnil, poslat na výstup řetězec ”Ahoj”. Pokud bychom chtěli vypsat např. určitou proměnnou, prostě místo řetězce napíšeme její jméno. Navíc operátorů pro výstup můžeme napsat libovolné množství – cout << "Velikost A je " << a << "." << endl. K endl se dostaneme vzápětí, pro tento okamžik konstatujme, že způsobí skok na nový řádek.
Na výstup můžeme posílat i speciální znaky jako \n – konec řádku, \t – tabulátor a jiné. stačí je prostě napsat do uvozovek k textu ("Jmeno \t Prijmeni \t Adresa").
Stejným způsobem jako vypisujeme na terminál, můžeme i načítat hodnoty zadané uživatelem. Načtení jednoho slova nebo čísla od uživatele provedeme pomocí operátoru >>. Všimněte si, že operátor ukazuje na opačnou stranu, než při na čítání. Směr je důležitý a nemůže být zaměněn
17
18
Základy jazyka C++
#include using namespace std; int main(){ int a, b; cout << "Zadej A: "; cin >> a; cout << "Zadej B: "; cin >> b; cout << "Zadana cisla jsou: " << a << " a " << b << endl; return 0;
}
Kromě výše uvedeného operátoru pro načítání disponuje C++ možností načítat více slov (např. celou větu). Jedná se o příkaz getline.
#include using namespace std; int main(void) { char s[100]; cout << "Napis retzec: "; cin.getline(s, 99); // getline neumí pracovat se stringem, musíme použít pole charů cout << "Napsal jsi " << s <<endl; return 0;
}
Příkaz getline může mít dva nebo tři parametry. Prví je vždy název proměnné, kam se budou data ukládat, druhý je maximální počet načtených znaků a třetí, nepovinný, je ukončovací symbol, který ukončí načítání (standardně ’\n’, tedy konec řádku). Načítáme vždy do pole znaků, které ovšem můžeme přeuložit do libovolného stringu (string retezec = s).
Manipulátory proudu Manipulátory nám umožňují modifikovat obsah proudu. Pro manip. s parametrem je potřeba použít knihovnu iomanip. S několika manipulátory jsme se již setkali.
• endl – konec řádku a vyprázdni bu er (při práci se souborem vyvolá fyzický zápis dat do souboru).
• flush – vyprázdni bu er. • dec, hex, oct – vypisuj všechna následující čísla v dané soustavě. • setw(x) – počet znaků, na které bude zarovnán vypisovaný text (využíváme při zarovnání čísel pod sebe podle řádů).
• setfill(’x’) – jakým znakem bude vyplňováno u zarovnání (standardně mezera). • ...
Základy jazyka C++
#include #include using namespace std; int main(void) { int c = 1000, b = 9; cout << setw(3) << "b=" << b << endl << "c=" << c << endl; cout << setw(3) << setfill(@) << hex << "c=" << c; ...
Práce s chybovým výstupem Kromě dvou výše uvedených vstupů a výstupů disponuje C++ ještě chybovým výstupem cerr. Na první pohled se chová stejně jako cout - tj. vypisuje informace na terminál. Ale pouze na první. Na úrovni operačního systému lze totiž oba proudy od sebe odlišit a s každým lze pracovat individuálně. Například chybový výstup lze přesměrovat do souboru pro pozdější analýzu. Proto je vhodné rozlišovat, kdy vypisujeme normální informaci a kdy chybovou hlášku, a podle toho zvolit příslušný proud. Přesměrování proudů na úrovni operačního systému Následující příkazy spolehlivě fungují pod OS UNIXového typu, ale zpravidla i pod Ms Windows. • Přesměruj výpis příkazu do souboru – ls>jmenoSouboru.txt • Přesměruj standardní výstup – program 1>jmenoSouboru.txt • Přesměruj chybový výstup – program 2>jmenoSouboru.txt • Zahod‘ chybový výstup – program 2>\dev\null • Přesměruj chybový výstup na standardní – 2>&1 Pravděpodobně Vás napadlo, proč se u posledního příkazu vyskytuje před jedničkou ”&”. Pokud bychom ho tam nedali, výstup by se přesměroval do souboru se jménem ”1”.
Příklad chybového výstupu.
#include using namespace std; int main(){ int a, b; cout << "Zadej A: "; cin >> a; cout << "Zadej B: "; cin >> b; if (b != 0) cout << "Podil pri celociselnem deleni: " << a/b << endl; else cerr << "Chyba, b = 0" << endl; return 0;
}
19
20
Objektově orientované programování
2
Objektově orientované programování
Základní pojmy a koncepty OOP
Svět je ”objektově orientovaný” Třída, objekt, instance třídy Základní myšlenkou objektově orientovaného programování (OOP) je pohled na program jako na systém objektů. Tento přístup je částo více intuitivní, protože svět přirozeně chápeme jako systém objektů. V objektově orientovaném programování je objekt samostatnou (autonomní) částí, která je vhodně propojena s ostatními objekty. Jednotlivé objekty pak mohou být více či méně přesnými modely okolního světa, přesněji té části světa se kterou daný program má pracovat. Objekt se skládá z vlastností a operací, které s ním lze provádět. Vlastnosti objektu se nazývají atributya slouží k uchovávání dat v objektu (říkáme k uchování stavu objektu). Funkce objektu se nazývají metody a určují vlastní chování objektu. Každý objekt je určen svou třídou. Třída objektu definuje všechny vlastnosti objektu a jejich přípustné hodnoty nebo rozsahy, definuje také funkce objektu, tedy jeho chování a reakce na okolí. Třída je abstrakní vzor (šablona) objektu, který definuje jaké atributy (data) a jaké metody (funkce) má každý z objektů příslušný k této třídě. Třída neobsahuje konkrétní hodnoty vlastností. Osobní automobil je třída objektů všech osobních automobilů.
Objekt je konkrétní část modelu, která funkce určené příslušnou třídou a konkrétní hodnoty vlastností. Jinými slovy: objekt je instancí třídy, v atributech definovaných příslušnou třídou má uloženy určité konkrétní hodnoty.
Bratrova Škoda Octavia je instancí třídy osobní automobil. Pozor: Škoda Octavia je opět třída všech automobilů Škoda Octavia.
Atribut objektu je určitá jeho vlastnost (i skrytá). Atrbibut objektu je definovaný ve třídě objektu, hodnota atributu je definovaná v objektu samotném. Atribut třídy je vlastnost, kterou mají všechny objekty dané třídy. Atribut třídy nemá definovanou žádnou konkrétní hodnotu, ale může mít zadaný rozsah přípustných hodnot.
Objem motoru - desetinné číslo je atribut třídy osobní automobil, který pouze definuje že všechna auta mají nějaký objem motoru a že datový typ této hodnoty je desetinné číslo. Jedná se o obecnou vlastnost. Objem motoru - 1.9l je atribut objektu, který definuje že např.: objekt bratrova Škoda Octavia má objem motoru 1.9l. Další vlastnosti třídy osobní automobil mohou být: váha, výkon, barva...
Zjednodušeně je možno říci, že objekt vznikne naplněním konkrétních hodnot do vlastností třídy. Každý objekt respektive jeho třídu lze popsat mnoha způsoby: • slovy - každé auto má váhu zookrouhlenou na celé kg, určitý objem motoru v litrech (desetinné číslo)... • diagramem (UML) • zápisem v programovacím jazyku
Objektově orientované programování Zápis třídy osobní automobil v jazyce C++:
class OsobniAutomobil { public: int vaha; float objemMotoru; ...
}
Zápis téže třídy v jazyce Java:
public class OsobniAutomobil { int vaha; double objemMotoru; ...
}
Na objekty se často pohlíží jako na modely určitých realných objektů. Pro modelování obecně platí, že vytvořený model by měl být co nejjednodušší a nejpřesnější. Před vytvářením modelu je tedy nutné znát jeho aplikaci, ze které pak vyplynou nezbytné vlastnosti modelu, univerzální model nelze vytvořit. Proto je i v případě objektového návrhu vhodné vytvářet objekty pouze s nezbytnými vlastnostmi. Dobře objektově navržené programy jsou snadno rozšiřitelné, přidávání dalšich atributů v průběhu vývoje programu není problém. Naopak vytváření objektů se zbytečnými atributy může vést ke složitým a nepřehledným definicím. Protože třída je pouze vzorem objektu a nikoliv konkrétním objektem, platí nasledující vztahy:
• Každý objekt musí patřit k určité třídě. Nelze vytvořit objekt, aniž by nebylo definováno jak má vypadat.
• Ze třídy může být vytvořen libovolný počet objektů (včetně 0). Podobně lze například podle výkresu vyrobit libovolný počet součástek.
• Místo pojmu objekt bývá často používán pojem instance třídy.
Změny stavů objektů Objekt by měl být samostatnou funkční jednotkou programu, proto zpravidla také obsahuje atributy (vlastnosti), které definují jeho aktuální stav. Většinou nesmí být stavové atributy měněny libovolně a proto současně objekt obsahuje i metody (funkce a procedury) pro jejich změnu. Změny stavu dosáhneme tedy zavoláním příslušné metody objektu 6 . Proces volání metod objektu se označuje jako zasílaní zpráv. Zpráva je interní programová struktura, která obsahuje jméno volané metody a její parametry. Po přijetí zprávy objekt vyvolá příslušnou metodu, výsledek uloží do struktury zprávy, která je pak doručena zpět odesílateli. Kličovou vlastností je nezávislost objektu na odesílateli zprávy (místě vyvolání metody objektu). Deklarace třídy OsobniAutomobil v jazyce C++, která obsahuje atributy i metody pro jejich nastavení a přečtení: 6 Při dodržování dusledné terminologie není možné používat spojení ”zavolání metody x”, protože metody spouští jedině objekt sám jako reakci na událost. V Praxi však tímto spojením myslíme ”zaslání zpávy objektu, která při zpracování spustí metodu x”
21
22
Objektově orientované programování
class osobniAutomobil { public: //atributy string barva; string spz; //deklarace metod - co bude mit trida za metody void nastavBarvu(string novaBarva); void nastavSPZ(string novaSPZ); void vypisBarvu(); void vypisSPZ(); }; // implementace metod, popisujeme jejich chovani // metoda pro nastaveni barvy void osobniAutomobil::nastavBarvu(string novaBarva){ barva = novaBarva;
} // metoda pro nastaveni SPZ void osobniAutomobil::nastavSPZ(string novaSPZ){ spz = novaSPZ;
} // metoda pro vypsani barvy void osobniAutomobil::vypisBarvu(){ cout << barva;
} // metoda pro vypsani SPZ void osobniAutomobil::vypisSPZ(){ cout << spz;
}
Část deklarace třídy OsobniAutomobil v jazyce C++ a ukázka vytvoření objektu:
Objektově orientované programování
class OsobniAutomobil { public: //atributy int rokVyroby; string spz; //metody void nastavRok(int rok); void nastavSPZ(string novaSPZ); void vypisSPZ(); void vypisRok(); }; void osobniAutomobil::nastavRok(int rok){ rokVyroby = rok;
} void osobniAutomobil::vypisSPZ(){ cout << spz;
} ...
//pouziti objektu // deklarace promenne, typem promenne je trida objektu osobniAutomobil *mojeSkodovka; mojeSkodovka = new osobniAutomobil; //vytvoreni objektu //nastaveni atributu mojeSkodovka->nastavRok(1981); mojeSkodovka->nastavSPZ("VYA-88-00"); //vypis atributu mojeSkodovka->vypisSPZ(); mojeSkodovka->vypisRok(); //zruseni objektu delete mojeSkodovka;
Atributy lze měnit buď přímo (instance->atribut = hodnota) nebo pomocí příslušných metod (instance->nastavAtribut(hodnota)).
Skrývání implementace Pro úplnou ochranu stavových atributů objektu je potřeba zabránit jejich modfikaci jinak než přes příslušné metody. Atributy a metody třídy jsou vždy přístupné z metod definovaných v této třídě, z jiných metod a funkcí však již přístupné být nemusí. K nastavení přístupu k atributům a metodám slouží modifkátory viditelnosti. Atributy označené public (veřejné) může kdokoli číst a modifikovat, metody označené public mohou být volány i z jiných metod a funkcí. Atributy označené private (soukromé) mohou číst a modifikovat pouze metody dané třídy, metody označené private mohou být volány pouze ze metod dané třídy. Pokud objekt obsahuje soukromé atributy a metody, tak pracuje jako černá skříňka, o které při pohledu zvenčí nelze zjistit jak pracuje, lze však vyvolávat některé její metody. Skutečná implementace funkcí objektu je ale zapouzdřena uvnitř objektu a je skryta.
23
24
Objektově orientované programování
Modifikátory viditelnosti: • private (přístupné pouze z metod této třídy) • protected (přistupné z metod této třídy a jejich potomků) • public (přístupné ze všech tříd, i hlavní funkce programu)
Modifikátory viditelnosti mění viditelnost pro všechny následující atributy a metody až do dalšího modifikátoru viditelnosti nebo do konce definice třídy. Výchozí modifikátor viditelnosti v jazyce C++ je private (tj. pokud není řečeno jinak).
class Konto { private: //nasledujici atributy a metody jsou soukrome int stavKonta; public: //nasledujici atributy a metody jsou verejne void nastavKonto(int novaSuma); void pridejCastku(int prirustek); int vratStavKonta(); void prictiUroky(); private: //nasledujici atributy a metody jsou soukrome void spoctiUroky(); };
Skrývání implementace je velmi důležitý rys objektového návrhu. Je velmi důležité jej důsledně dodržovat.
Zdaleka ne všechny objektové jazyky podporují tento koncept skrývání implementace. Například v jazyku Python se pro skrytí atributu nebo metody používá ”zdobení” – před název atributu nebo metody jsou napsány dvě podtžítka (např. skrytyAtribut). Takový atribut je intepretem pouze přejmenován, aby nemohlo dojít k jeho náhodnému zavolání. Lze jej však přesto zavolat pomocí nového jména. Přesný ekvivalent private tedy v Pythonu nenajdeme.
Shrnutí • • • • • • •
Program je systém objektů a vazeb mezi nimi. Třídy definují typy objektů. Objekty jsou samostatné funkční celky (černé skříňky). Stav objektu je popsán jeho atributy Funkce objektu je definována jeho metodami Skrývání (části) implementace objektu umožňují modifikátory viditelnosti Vazby mezi objekty zajišťuje systém zasílání zpráv Objekt je tedy programová struktura, která má následující vlastnosti: • má vnitřní pamět (ta může obsahovat i další objekty), • obsahuje metody, • je schopna přijímat a zpracovát zprávy z vnějšku tak, že vyvolá příslušnou metodu.
25
Objektově orientované programování
Řešené příklady k procvičení problematiky návrhu tříd 1. Pojem třída, objekt, atribut Popis: Svět okolo nás můžeme modelovat pomocí objektů. Poznámka: Pokud jste v programování začátečníci, nezabývejte se takovou hloupostí, jako jsou výčtové typy, prostě na popis např. materiálu použijte textovou prom. typu string. Výčtový typ nám pouze říká, že materiál může být pouze jeden z n možných, což v tomto okamžiku není zásadní. a) Teoretický rozbor
Popište slovy reklamní tužku z obchodního řetězce Ikea. Vytvořte model této tužky a pojmenujte jednotlivé atributy (vlastnosti). Zapište vlastnosti a jejich hodnoty a obecné datové typy ( řetězec, číslo (celé, reálné), datum, výčet, struktura, binární hodnota) do tabulky. Tip: Soustřeďte se na zřejmé vlastnosti, nehledejte v tom vědu. K zamyšlení: Má tyto vlastnosti každá tužka?
Krátká dřevěná tužka s černou měkkou tuhou a modrým nápisem ikea. název atributu
hodnota atributu
datový typ
délka tužky
8 cm
číslo
materiál
dřevo
výčet
barva tuhy
černá
výčet
tvrdost tuhy
měkká
výčet/číslo
barva nápisu
modrá
výčet
text nápisu
ikea
řetězec
Závěr: Tyto vlastnosti má každá tužka, tyto atributy tedy můžeme definovat obecně pro různé tužky. Z hlediska modelu můžeme tvrdit, že i nápis má každá tužka, pouze u některých tužek neobsahuje nápis žádný text a není tedy vidět. Současně však je možné definovat velké množství dalších atributů podle konkrétního využití modelu. Obecnou definicí atributů nazýváme třídou a pojmenujeme ji Tuzka. Třída Tuzka je nehmotná šablona všech tužek a říká nám, že cokoliv co je tužkou má určité atributy (délka, materiál, barva tuhy) a že tyto atributy mohou nabývat určitých hodnot (datových typů).Tužka z Ikei (tuzkaIkea) je pak konkrétní objekt, je to instance třídy, ve které jsou atributům přiřazeny konkrétní hodnoty. b) Programové řešení
Napište program, který definujte třídu Tuzka, definujte objekt této třídy tuzkaIkea, naplňte všechny atributy objektu a pak je vypište na obrazovku. Definujte také potřebné výčtové typy.
#include //standardni hlavicka , umozni vypis na obrazovku using namespace std; //standardni hlavicka //deklarace povinnych vyctovych typu enum Barvy {CERVENA, MODRA, ZELENA, BILA, CERNA}; enum Materialy {DREVO, KOV}; //deklarace tridy class Tuzka { public: //nasledujici polozky v teto tride jsou verejne //atributy
26
Objektově orientované programování
int delka; Materialy material; Barvy barvaTuhy; int tvrdostTuhy; Barvy barvaNapisu; string napis;
}; //===== hlavni funkce programu int main() { Tuzka *tuzkaIkea; //promenna pro ulozeni objektu /* nelze pristupovat k objektu, ktery neni vytvoreny, nasledujici radek by tedy zpusobil pad programu cout << "\nDelka: " << tuzkaIkea->delka; */ tuzkaIkea = new Tuzka; //vytvoreni objektu - dojde ke spusteni konstruktoru tridy Tuzka //naplneni atributu objektu tuzkaIkea->delka = 8; tuzkaIkea->material = DREVO; tuzkaIkea->barvaTuhy = CERNA; tuzkaIkea->barvaNapisu = MODRA; tuzkaIkea->tvrdostTuhy = 3; tuzkaIkea->napis = "Ikea"; //vypsani atributu objektu cout << "Delka: " << tuzkaIkea->delka << " cm" << endl; cout << "Material: " << tuzkaIkea->material << endl; cout << "Barva tuhy: " << tuzkaIkea->barvaTuhy << endl; cout << "Tvrdost tuhy: " << tuzkaIkea->tvrdostTuhy << endl; cout << "Barva napisu: " << tuzkaIkea->barvaNapisu << endl; cout << "Napis: " << tuzkaIkea->napis << endl; delete tuzkaIkea; //zruseni objektu - spusteni destruktoru tridy Tuzka /* nelze pristupovat k objektu, ktery je zruseny, nasledujici radek by vypsal nesmyslny udaj cout << "\nDelka: " << tuzkaIkea->delka; */ return 0;
} 2. Pojmy třída, objekt, atribut, metoda, modifikátory viditelnosti Popis: Objekty umožňují kromě ukládání dat, také uchovávat části programu, díky tomu mohou objekty pracovat jako samostatné moduly programu. a) Teoretický rozbor a programová realizace první části
Popište bílý list papíru A4 na kterém je černým fixem napsán text „Něco duchaplného“. Definujte třídu tohoto objektu, pojmenujte jednotlivé atributy a zapište jejich datové typy. Definujte alespoň 10 atributů. Napište program, který definujte třídu ListPapiru definujte objekt této třídy, naplňte atributy objektu a pak je vypište na obrazovku. Definujte také potřebné výčtové typy. Tip: Zamyslete se nad závislostmi jednotlivých atributů.
název atributu
hodnota atributu
datový typ
formát
A4
výčet
text
„Něco duchaplného“
řetězec
barva papíru
bílá
výčet (pokračování tabulky na další straně)
27
Objektově orientované programování
barva textu
černá
výčet
psací potřeba
fix
výčet
šířka
210 mm
číslo
výška
297 mm
číslo
hmotnost
80 mg
číslo
tloušťka
0,05 mm
číslo
orientace textu
vodorovně
výčet
class ListPapiru { public: //nasledujici polozky v teto tride jsou verejne //atributy Format format; string text; Barva barvaPapiru; Barva barvaTextu; Pisatko pisatko; int sirka; int vyska; float tloustka; int hmotnost; Orientace orientace; }; Závěr: Některé z atributů uvedených v tabulce jsou na sobě přímo závislé, například šířka, výška a formát. Bylo by možné atribut formát vypustit, protože je plně určen výškou a šířkou. Prakticky je však atribut formát výhodné ponechat a zajistit aby nedošlo k nesouladu mezi jeho hodnotou a hodnotou ostatních atributů. Například při tisku z textového editoru je možné zvolit buď velikost papíru nebo formát papíru, druhá hodnota je pak automaticky dopočítána. Další možnou závislostí je například barva papíru a barva textu, tyto dvě barvy by neměly být stejné. b) Programová realizace druhé části
Třídu ListPapiru rozšiřte o přístupové metody k atributům formát, výška, šířka. Metody zajistí správné nastavení formátu papíru při změně šířky nebo výšky a naopak správné nastavení výšky a šířky při změně formátu. Metody pro čtení (zjištění hodnoty) atributů se označují getAtribut, metody pro uložení (nastavení hodnoty) se označují setAtribut. Současně vhodně definujte modifikátory viditelnosti private (soukromý) a public (veřejný) k zamezení/povolení přístupu k atributům a metodám třídy.
enum Format {A0, A1, A2, A3, A4, VLASTNI}; class ListPapiru { private: /* nasledujici polozky jsou pristupne jen z metod deklarovanych v teto tride, nebudou tedy pristupne z jinych trid ani z funkce main() */ /* atributy format, vyska a sirka jsou skryte, pristup k nim je mozny pouze pres verejne metody getFormat, setFormat, getVyska, setVyska, getSirka, setSirka */ int sirka; int vyska; public: //nasledujici polozky v teto tride jsou verejne //atributy string text;
Format format;
28
Objektově orientované programování
tBarva barvaPapiru; tBarva barvaTextu; tPisatko pisatko; float tloustka; int hmotnost; tOrientace orientace; void setFormat(Format value); void setVyska(int value); void setSirka(int value); Format getFormat(); int getVyska(); int getSirka();
}; 7
Závěr: Příklad ukazuje využití zapouzdření. Atributy, které nelze libovolně měnit je vhodné skrýt (pomocí modfikátorů viditelnosti) a přístup k nim umožnit pouze přes metody. 3. Atributy pouze pro čtení
Definujte třídu Sklenice, která popisuje válcovou sklenici bez potisku z hlediska uživatele. Definujte konstruktor a desturktor třídy, definujte vzájemě závislé (a přesto užitečné atributy) a metody pro jejich čtení a zápis. Zapište jednotlivé atributy do tabulky.
název atributu
hodnota atributu
datový typ
výška
10 cm
číslo
průměr
8 cm
číslo
objem
500 cm
číslo
materiál
čiré sklo
výčet
class Sklenice { private: /*nasledujci polozky jsou pristupne pouze ze tridy Sklenice */ int vyska; int prumer; float objem; /* definice atributu objem neni nezbytna */ public: //nasledujici polozky v teto tride jsou verejne //atributy tMaterial material; //metody int getVyska(); int getPrumer(); void setVyska(int value); void setPrumer(int value); float getObjem(); /* atribut objem nelze nastavit, neni verejne pristupny a nema metodu pro nastaveni */ }; 8
Závěr: Mohlo by se zdát, že se jedná o stejný případ jako nastavení formátu u listu papíru. V tomto případě je sice objem jednoznačně určen výškou a průměrem sklenice (stejně jako formát je jednoznačně určen výškou a šířkou papíru), rozměry sklenice však nejsou jednoznačně určené objemem. Samotné nastavení objemu tedy ztrácí smysl a objem v tomto 7 8
Úplný zdrojový kód a druhé řešení programu jsou uvedeny v příkladu Papír. Úplný zdrojový kód a druhé řešení programu jsou uvedeny v příkladu Sklenice.
Objektově orientované programování případě definujeme jako atribut, který je pouze pro čtení. 9 Protože je atribut objem plně určen atributy výška a šířka, není nutné ho vůbec definovat, může to však být výhodné v případě, že zjištění hodnoty je výpočetně nebo časově náročná operace.
9
Je možné definovat i atributy pouze pro zápis. Jedná se však o velice specifické případy využití.
29
1
Objektově orientované programování
Test Způsob vyhodnocení: Při vyhodnocení budou za nesprávné odpovědi strhnuty body.
1. Zaškrtněte mezi možnostmi všechny položky, které v následujícím kódu označují třídu : class Motorka { public: int serioveCislo; string vyrobce; }; int main(){ Motorka* jawa; jawa = new Motorka; delete(jawa);
1
} Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d)
serioveCislo Motorka jawa vyrobce
2. Zaškrtněte mezi možnostmi všechny položky, které v následujícím kódu označují objekty : class Motorka { public: int serioveCislo; string vyrobce; }; int main(){ Motorka* jawa; jawa = new Motorka; delete(jawa);
1
} Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d)
jawa vyrobce Motorka serioveCislo
3. Zaškrtněte mezi možnostmi všechny položky, které v následujícím kódu označují atributy : class Motorka { public: int serioveCislo; string vyrobce; }; int main(){ Motorka* jawa; jawa = new Motorka; delete(jawa);
} Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) serioveCislo b) vyrobce c) Motorka
1
2
Objektově orientované programování d) jawa 4.
Rád bych do sériového čísla určité motorky (např. níže uvedené jawy) nastavil určitou hodnotu. Vyberte, jak to mám udělat (tj. jaký řádek se má nacházet v kódu místo pomlček). class Motorka { public: int serioveCislo; string vyrobce; }; int main(){ Motorka* jawa; jawa = new Motorka; --delete(jawa);
1
} Vyberte jen jednu z následujících možných odpovědí.
a) jawa->serioveCislo = 123456; b) serioveCislo = 123456; c) Motorka->serioveCislo = 123456; 5. Zaškrtněte mezi možnostmi všechny položky, které v následujícím kódu označují instanci třídy : class Motorka { public: int serioveCislo; string vyrobce; }; int main(){ Motorka* jawa; jawa = new Motorka; delete(jawa);
1
} Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d)
jawa Motorka vyrobce serioveCislo
Konstruktor, destruktor
Život objektu Život objektu začíná jeho vytvořením a končí jeho zrušením. Vytvoření objektu reprezentuje rezervování vhodného paměťového prostoru podle definice třídy a případně inicializaci atributů objektu. Zrušení objektu pak reprezentuje uvolnění rezervované paměti a provedení dalších činností (vypsání hlášení, uzavření souborů, atp.). K těmto speciálním okamžikům v životě objektu se vztahují speciální metody – konstruktor a destruktor. Konkrétní podoba těchto metod a jejich použití se může lišit v různých objektově orientovaných jazycích.
Konstruktor Konstruktor je metoda volaná při vytváření instance třídy, typicky se využívá pro inicializaci objektu. Název konstruktoru je stejný jako název třídy, konstruktor nemá návratový typ a nelze ho volat přímo jako jiné metody. Deklarace konstruktoru není povinná. Při vytváření instance třídy, která nemá definovaný žádný konstruktor se použije implictní konstruktor (bez parametrů). Konstruktor je automaticky vyvolán při vytvoření instance pomocí klíčového slova new (v C++, Javě).
Objektově orientované programování
Deklarace a použití konstruktoru v C++:
class Krida { public: // konstruktor Krida(){ cout << "Ahoj, ja jsem pan konstruktor" << endl;
} } ... Krida *mojeKrida; mojeKrida = new Krida; //vytvoreni objektu - volani konstruktoru
Je také možné definovat pro jednu třídu více konstruktorů, protože jméno konstruktoru je stejné jako jméno třídy, musí se jednotlivé konstruktory lišit parametry. Tato technika se nazývá přetěžování a je podrobněji popsána v kapitole Pokročilé techniky OOP.
Parametrický konstruktor Konstruktor je možné definovat také s parametry, to je výhodné pro snadnější inicializaci objektů (není nutné nastavovat každý atribut zvlášť). Parametrický kontruktor navíc de facto vynutí zadání parametrů a vždy provede inicializaci. Je tedy vhodný pro vynucení zadání hodnot, na které se nesmí zapomenout. Parametrický konstruktor je také možné využít v souvislosti se zapouzdřením. Pokud má objekt atributy, které nelze v průběhu života objektu měnit, pak je výhodné je vytvořit k nim metody pouze pro čtení a jejich počáteční nastavení zajistit prostřednictvím konstruktoru s parametry.
class PujceneAuto{ float natankovano; int ujetychKm; public: //implementace konstruktoru pro inicializaci atributu PujceneAuto(float kolikNatankuji){ natankovano = kolikNatankuji; ujetychKm = 0;
} } ... PujceneAuto *fiesta; fiesta = new PujceneAuto(35.7); //volani konstruktoru s parametry
Konstruktor může být bezparametrický nebo s parametry (říkáme parametrický). Jedna třída může mít i více konstruktorů lišících se parametry.
Destruktor Destruktor je metoda, která se automaticky spouští při rušení objektu. Název destruktoru začíná znakem vlnovky ˜. Jeho název je stejný jako název třídy. Stejně jako konstruktor nemá destruktor návratový typ a nelze ho přímo volat. Destruktor navíc nikdy nemá parametry! Destruktor se nejčastěji využívá pro uvolnění paměti objektu v případě, že jsou v objektu definované atributy, které implicitní konstruktor neuvolní správně (ukazatele, složené typy, seznamy, ...).
3
4
Objektově orientované programování
Deklarace destruktoru v C++: class Krida() { Krida(){ //bezparametricky konstruktor cout << "Prave zaciname" << endl;
} ~Krida(){ // destruktor cout << "Ahoj, koncim" << endl;
} } ... Krida *mojeKrida; mojeKrida = new Krida; //vytvoreni objektu - volani konstruktoru delete mojeKrida; //zruseni objektu - volani destruktoru
Destruktor je vždy bezparametrický a může být pouze jeden.
Jaký význam mají konstruktor a destruktor?
Řešené příklady k procvičení problematiky 1. Parametrický konstruktor, destruktor a) Teoretický rozbor
Definujte třídu CD, která popisuje objekt kompaktní disk. Definujte alespoň šest atributů včetně typů, viditelností a přístupových metod. Objekt uvažujte z hlediska spotřebitele. Definujte parametrický konstruktor, jehož parametry jsou všechny hodnoty, které nelze u již vytvořeného objektu měnit.
název atributu
hodnota atributu
typ atributu
průměr
100 mm
číslo
kapacita
703 MB
číslo
povrch
hladký
výčet
barva
stříbrná
výčet
popis
„Moje CD“
řetězec
barva popisu
černá
výčet
výrobce
Verbatim
řetězec
Závěr: Některé atributy objektu jsou z principu nastavitelné pouze jednou a to právě při vytváření objektu. Současně je však potřeba neustále uvažovat o aplikaci modelu. V případě kompaktního disku se během jeho výroby se některé atributy mění (velikost, barva). Z hlediska spotřebitele však není vůbec možné změnit průměr nebo materiál CD bez jeho
Objektově orientované programování zničení. Nelze tedy zaměňovat skutečný vznik objektu a vznik modelu, vytvoření instance třídy CD (z hlediska spotřebitele) odpovídá zakoupení kompaktního disku, nikoliv jeho vyrobení. Proto je možné a vhodné nastavit parametry, které jsou z hlediska uživatele neměnné pouze jednou, tedy při vytváření objektu, ideální místo pro toto nastavení je v konstruktoru objektu. b) Programové řešení části parametrický konstruktor
Napiše program v C++, který definuj třídu CD s destruktorem a s parametrckým konstruktorem, jehož parametry jsou všechny hodnoty, které nelze u již vytvořeného objektu měnit. Vytvořte objekt této třídy. Tip: Nezapomeňte, že třída je pouze přibližným modelem reality.
enum tPovrch {HLADKY, DRSNY}; enum tBarva {BILA, STRIBRNA, CERNA, MODRA}; class CD { // deklrace tridy private: // nasledujci polozky jsou pristupne pouze z tridy CD int prumer; int kapacita; tPovrch povrch; string vyrobce; tBarva barva; public: //nasledujici polozky v teto tride jsou verejne tBarva barvaPopisu; string popis; //metody CD(int zadPrumer, int zadKapacita, tPovrch zadPovrch, string zadVyrobce, tBarva zadBarva); //konstruktor ~CD(); //destruktor int getPrumer(); //metody pro cteni atributu int getKapacita(); tPovrch getPovrch(); string getVyrobce(); tBarva getBarva(); }; //konstruktor tridy CD::CD(int zadPrumer, int zadKapacita, tPovrch zadPovrch, string zadVyrobce, tBarva zadBarva) { cout << "Byl vytvoren objekt tridy CD.\n"; prumer = zadPrumer; povrch = zadPovrch; kapacita = zadKapacita; vyrobce = zadVyrobce; barva = zadBarva;
} //destruktor tridy CD::~CD() { cout << "\nByl zrusen objekt tridy CD.";
} //zjisteni hodnoty prumeru int CD::getPrumer() { return prumer;
} //zjisteni kapacity int CD::getKapacita() { return kapacita;
5
6
Objektově orientované programování
} //zjisteni typu povrchu tPovrch CD::getPovrch() { return povrch;
} //zjisteni vyrobce string CD::getVyrobce() { return vyrobce;
} //zjisteni barvy tBarva CD::getBarva() { return barva;
} int main(int argc, char *argv[]) { CD *prazdneCD; // promenna // vytvoreni objektu prazdneCD = new CD(100, 702, HLADKY, "Verbatim", STRIBRNA); //naplneni objektu prazdneCD->barvaPopisu = CERNA; prazdneCD->popis = ""; //vypsani objektu cout << "\nPrumer: " << prazdneCD->getPrumer(); cout << "\nKapacita: " << prazdneCD->getKapacita(); cout << "\nPovrch: " << prazdneCD->getPovrch(); cout << "\nVyrobce: " << prazdneCD->getVyrobce(); cout << "\nBarva: " << prazdneCD->getBarva(); cout << "\nBarva popisu: " << prazdneCD->barvaPopisu; cout << "\nPopis: " << prazdneCD->popis; delete prazdneCD; //zruseni objektu
} Závěr: Parametry konstruktoru je nutné odlišit od názvů atributů uvnitř třídy, jednou možností je prefix u názvu parametrů, například ”a” (argument). Jinou možností je přistupovat k atributům třídy v konstrukoru pomocí speciální proměnné this, její použití je popsáno v kapitole Pokročilé techniky OOP. 2. Parametrický konstruktor, destruktor
Příklad nám ukazuje medvěda, který se vydal na lov, aby si nastřádal kalorie na zimní spánek. Potkava různé ”chody”, které konzumuje a ty mu přidávají/ubírají kalorie a život.
#include using namespace std; class Medved{ private: // dali jsme je private, protoze je budeme menit pouze pomoci metody snez // jinak nemaji jit modifikovat int zdravi; int kalorie; public: // pokud nerekneme jinak, bude mit me2d na pocatku 50 kalorii Medved(){ zdravi = 100; kalorie = 50; cout << "Mame noveho me2da s 50ti kaloriemi" << endl;
Objektově orientované programování
} // ale muze urcit jiny pocet Medved(int pocatecniKalorie){ zdravi = 100; kalorie = pocatecniKalorie; cout << "Mame noveho me2da s " << kalorie << "ti kaloriemi" << endl;
} void snez(int zadaneKalorie, int zadaneZdravi){ zdravi = zdravi + zadaneZdravi; kalorie = kalorie + zadaneKalorie;
} // mohou si udelat sami doma void vypisInfoOMedvedovi(){ cout << "Nas me2d ma " << kalorie << " kalorii a "; cout << zdravi << "% zdravi." << endl;
} ~Medved(){ cout << "Nas me2d sel spat s " << kalorie << "ti kaloriemi v brise" << endl;
} }; class Jidlo{ private: // tyto hodnoty jsme dali private, protoze maji byt pouze ke cteni // pri vytvoreni objektu se zadaji a dale uz se nesmi menit int kalorie; int zdravi; public: Jidlo(int zadaneKalorie, int zadaneZdravi){ zdravi = zadaneZdravi; kalorie = zadaneKalorie;
} int vratKalorie(){ return kalorie;
} int vratZdravi(){ return zdravi;
} ~Jidlo(){ cout << "a je po jidle..." << endl;
} }; int main(void){ Medved* brumla; brumla = new Medved(100); // turista je kurak, proto pridava kalorie, ale ubira zdravi // pripomenout, ze deklarace a inicializace objetu se da spojit Jidlo* turista = new Jidlo(5000, -30); brumla->snez(turista->vratKalorie(),turista->vratZdravi()); delete(turista); // turista snezen brumla->vypisInfoOMedvedovi();
7
8
Objektově orientované programování
// myska by lehka, ale pomerne zdrava Jidlo* mys = new Jidlo(60, 10); brumla->snez(mys->vratKalorie(),mys->vratZdravi()); delete(mys); // mys uz byla snezena // brumla->vypisInfoOMedvedovi(); delete(brumla); // konec me2da return 0;
}
1
Objektově orientované programování
Test Způsob vyhodnocení: Při vyhodnocení budou za nesprávné odpovědi strhnuty body.
1. Máme následující třídu v jazyku C++. Doplňte obsah metody vypisObsahUctu() tak, aby metoda vypsala na obrazovku obsah atributu stavUctu. class Ucet{ private: string IBAN; int stavUctu; public: void ulozNovyStavUctu(int castka); void ulozIBAN(string IBAN); void vypisIBAN(); void vypisStavUctu(); };
1
Vyberte jen jednu z následujících možných odpovědí.
a) b) c) d)
cout << stavUctu; return stavUctu; return vypisStavUctu() cout << vypisStavUctu() << endl;
2. Co se vypíše na obrazovku? #include using namespace std; class Reditel{ private: int plat; public: string jmeno; string oddeleni;
int vratPlat(){ return plat;
} Reditel(){ plat = 10000; jmeno = "Jan Novak";
} ~Reditel(){ cout << "ja se vratim" << endl;
} }; int main(){ Reditel* novak; novak = new Reditel; novak->oddeleni = "graficke"; novak->jmeno = "Petr Novak"; cout << novak->jmeno << endl; cout << novak->vratPlat() << endl; cout << novak->oddeleni << endl; delete(novak); return 0;
} Vyberte jen jednu z následujících možných odpovědí.
a) Jan Novak 10000 graficke
1
2
Objektově orientované programování b) Petr Novak 10000 graficke ja se vratim c) nic (program se nepřeloží) nebo něco jiného d) Petr Novak 10000 graficke e) Jan Novak 10000 graficke ja se vratim 3. Co se vypíše na obrazovku? #include using namespace std; class Reditel{ private: int plat; string jmeno; string oddeleni; public: int vratPlat(){ return plat;
1
} string vratJmeno(){ return jmeno;
} string vratOddeleni(){ return oddeleni;
} Reditel(){ plat = 10000; jmeno = "Jan Novak"; oddeleni = "graficke";
} ~Reditel(){ cout << "ja se vratim" << endl;
} }; int main(){ Reditel* novak; novak = new Reditel; cout << novak->jmeno << endl; cout << novak->plat << endl; cout << novak->oddeleni << endl; delete(novak); return 0;
} Vyberte jen jednu z následujících možných odpovědí.
a) nic (program se ani nepřeloží) nebo něco jiného b) Jan Novak 10000 graficke ja se vratim c) Jan Novak 10000 graficke 4. Pokud přistupujeme k atributu hotovost z následujícího kódu z hlavní funkce programu (main) nebo metod jiných tříd, platí (nezapomeňte, že s atributy lze pracovat přímo i prostřednictvím metod):
1
3
Vztahy
class Bankomat{ private: int hotovost; int cisloBankomatu; public: int vratCislo(){ return cisloBankomatu;
} void vypisCislo(){ cout << "Cislo bankomatu: " << cisloBankomatu << endl;
} void uberCastku(int kolik){ hotovost = hotovost - kolik;
} void zadejCelkovouCastku(int suma){ hotovost = suma;
} }; int main(){ ...
} Vyberte jen jednu z následujících možných odpovědí.
a) b) c) d)
do atributu lze pouze zapisovat atribut lze číst i do něj zapisovat atribut nelze číst ani do něj zapisovat atribut lze pouze číst
5. Mám následující kód třídy v jazyku C++, jakým způsobem mohu vytvořit instance této třídy? class Auto(){ private: string SPZ; int rokVyroby; public: Auto(){ SPZ = ""; rokVyroby = "2000";
} Auto(string novaSPZ, int novyRokVyroby){ SPZ = novaSPZ; rokVyroby = novyRokVyroby;
} // zde se budou nachazet metody pro cteni a zapis do atributu ... }; Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) Auto* skoda b) Auto* skoda c) Auto* skoda d) Auto* skoda
skoda; = new Auto; skoda; = new Auto(1998, "VYA 88-00"); skoda; = new Auto(" ", 2000); skoda; = new Auto();
1
4
Objektově orientované programování
2.1
Vztahy
Asociace
V předchozích kapitolách byly všechny objekty modelovány jako jednoduché, tedy vlastnosti objektů nebyly nijak strukturované a objekty vzájemně nebyly nijak propojeny. Tento přístup je prakticky nepoužitelný, protože bez vazeb mezi objekty nelze vytvořit objektový model. Vazby mezi objekty současně také ovlivňují vlastní definice objektů.
Asociace Asociace je nejvolnějším typem vazby mezi objekty, umožňuje objektům vzájemnou komunikaci. Asociované objekty si mohou vzájemně zasílat zprávy (tj. volat svoje metody). Základní charkteristikou 10 všech vztahů je násobnost (kardinalita). Násobnost vazby je definovaná násobností každého konce vazby a je možné ji vyjádřit pomocí následujících symbolů: 11
• *, N, M - vazby se může učástnit libovolný počet instancí • 1 - vazby se může učástnit pouze jedna instance • 0 - vazby se neučastní žádná instance Tyto základní symboly je pak možné kombinovat vytvořením intervalu:
• 0..1 - může být navázána jedna nebo žádná instance • 1..* - může být navázáno N instancí, minimálně však jedna • ... Varianty násobnosti vazeb je možné obecněji rozdělit do tří kategorií vztahů.Vztah 1:1 označuje vazby, ve kterých je každá instance jedné třídy je spojena s nejvýše jednou instancí jiné třídy. Tento vztah je tedy jednoznačný v obou směrech.Obecnějším vztahem je vztah 1:N (M:1), ve kterém je každá instance první třídy spojená s libovolným počtem instancí druhé třídy. Současně však platí, že každá instance druhé třídy je spojená s jednou nebo žádnou instancí první třídy. Vztah 1:N je tedy jednoznačný pouze v jednom směru (od druhé třídy k první). Nejobecnějším vztah M:N, který je nejednoznačný v obou směrech, protože každá instance jedné i druhé třídy může být spojena s libovolným počtem jiných instancí.
10
Jedná se o stejnou charakteristiku, která se používá při návrhu relačních databaází. Uzly v UML diagramech repretezentují třídy objektového modelu, současně však reprezentují tak všechny možné instance této třídy. Nakreslením vazby mezi třídami tedy vlastně zobrazujeme vazbu mezi jednotlivými instancemi tříd. 11
5
Vztahy
Zobrazení vazby mezi obyvatelem a obcí.
Vazbu na obrázku je možné popsat následujícími větami: • Obyvatel žije v jedné nebo v žádné obci. • V obci žije libovolný počet obyvatelů, minimálně však jeden. • Obyvatel nemůže žít ve více obcích současně. • Vztah mezi obyvatelem a obcí je N:1 Při definici vazby je nutné (stejně jako při definici atributů) uvažovat konkrétní použití tříd i celého modelu. Mezi obyvatelem a obcí je možné definovat také vztah N:M popsaný následujícími body:
• Obyvatel žije v jedné nebo v žádné obci. • V obci žije libovolný počet obyvatelů, minimálně však jeden. • Obyvatel mohl žít ve více obcích
Jedná se tedy o znázornění vztahu obyvatele k obci v průběhu času. Obyvatel mohl žít ve více obcích, protože se přestěhoval, v jednom časovém okamžiku však žije pouze v jedné obci (není na diagramu zachyceno).
V diagramu tříd vztah Asociace definuje obecnou vazbu mezi objekty, nepoužívá se k vyjádření konkrétních činností nebo akcí. Tedy asociace v předchozím příkladu ”žije v” v sobě zahrnuje všechny možné činnosti, které by obyvatel města mohl ve městě vykonávat (pracuje, bydlí, nakupuje). Asociace mezi třídami tedy nejsou paralelní, tedy mezi dvěma třídami je vždy maximálně jedna asociace. Také se nedefinují vazby inverzní (například ”ve městě žije”) asociace. Důvodem je fakt, že obecně je možno mezi třídami definovat (téměř) nekonečně mnoho různých vztahů, proto by měl popis vztahu asociace být natolik obecný, aby zahrnoval všechny modelované vazby.
6
Objektově orientované programování
Vztah mezi osobou a židlí (například pro sledování obsazenosti učebny nebo kina).
Asociaci je možné popsat následujícími větami: • jedna osoba může sedět na jedné židli • osoba nemusí sedět na žádné židli • na židli nemusí nikdo sedět • na židli může sedět jedna osoba Kardinalita vazby je 1:1, tedy k jedné osobě lze přiřadit jednu nebo žadnou židli a naopak. Toto odpovídá zadání, ve kterém sledujeme obsazenost místnosti a nesnažíme se postihnout všechny reálně možné případy (jedna osoba sedí na více židlích a na jedne židli sedí více osob). Většinu vazeb je možno zahrnutím krajních případů rozšířit na vazby M:N. Stejně jako v předchozím případě je definovaná pouze vazba ” sedí na”, která zahrnuje všechny ostatní vztahy které mohou mezi třídami Osoba a Židle nastat.
Reflexivní asociace
V některých případech mohou objekty téže třídy komunikovat vzájemně. V diagramu tříd tuto vazbu znázorňujeme jako asociaci jejíž oba konce jsou v dané třídě. V diagramu objektů se pak asociace mezi objekty téže třídy zobrazují stejně jako mezi objekty různých tříd.
7
Vztahy
Vytvořte diagramy tříd Učitel a Student a definujte vztahy asociace ”učí” a ”zná se”. Vytvořte diagram objektů tříd Učitel a Student a definujte mezi nimi vazbu asociace. Řešení: Vazba ”učí” mezi učitelem a studentem je vazba typu N:M. Vazbu ”zná se” nedefinujeme mezi učitelem a studentem (je zahrnuta ve vztahu ”učí”), ale mezi jednotlivými studenty. Ti se navzájem mohou a nemusí znát, každý student také může znát libovolný počet jiných studentů, jedná se tedy opět o vztah s násobností M:N. Vdiagramu tříd jsou znázorněné obecné vazby mezi třídami objektů: • učitel může učit jednoho a více studentů • studenta může učit jeden a více učitelů • student může znát libovolný počet jiných studentů • studenta může znát libovolný počet jiných studentů
V objektovém diagramu jsou pak vyznačené konkrétní vztahy, tedy:
• student Franta se zná s Pepou i Květošem • Pepa a Květoš se neznají • učitel všechny učí
8
Objektově orientované programování
Implementace Konkrétní implementace asociace je poměrně jednoduchá. Jak bylo řečeno asociace je voláním metody jiného objektu (zasílání zprávy). Stačí tedy jednomu objektu ”doručit” odkaz na objekt, jehož metodu má zavolat.
Zadání: Zapište deklaraci třídy Ucitel a Student. Učitel bude moci zasilat zpravy určitému žákovi.
class Student{ public: // atributy string jmeno; // metody void vypisInfo(){ cout << "jmeno: " << jmeno << endl;
} }; class Ucitel{ public: // metode predavam odkaz na objekt tridy Student, // tento odkaz jsem pojmenoval zak void informujOStudentovi(Student* zak){ // volam metodu jineho objektu zak->vypisInfo();
} }; int main(){ Ucitel* karasek = new Ucitel; Student* omacka = new Student; omacka->jmeno = "Bedrich Omacka"; // v tomto okamziku se realizovala vazba mezi karaskem a omackou // metoda karaska zavolala metodu omacky a vypsalo se jmeno karasek->informujOStudentovi(omacka); return 0;
}
Užitečný model je takový model, který je co nejjednodušší a přitom dobře (nikoliv ideálně) postihuje modelovanou skutečnost.
Je nutné dodat, že termínu asociace se v literatuře často využívá v okamžiku, kdy mluvíme o volání metody (jak je ukázáno výše), ale také v okamžiku kdy mluvíme o ”obecné vazbě” jejíž typ ještě nebyl upřesněn.
9
Vztahy
Řešený příklad k procvičení
Definujte třídu Auto a další tři třídy osob tak, aby každá z nich byla ke třídě Auto vázána asociací s jinou násobností. Řešení: Příkladem osoby, která ma k autu jednoznačný vztah 1:1 je řidič auta. Řidič může řídit maximalně jedno auto a jedno auto nemohou řídit dva řidiči. Naproti tomu, mezi pasažérem a autem je vztah 1:N, protože v automobilu může cestovat více pasažerů. Jeden pasažér však může cestovat pouze v jednom vozidle. Příkladem osoby se vztahem M:N může být uživatel automobilu (tj. osoba, která od něj má klíče), tedy jedno vozidlo může být užívano několika uživateli, současně však každý uživatel může užívat několik vozidel (do této skuipny je možné zařadit také například mechanika, policistu a další).
Alternativně by bylo možné definovat násobnosti vazeb ”řídí” a ”jede v” na straně třídy Auto jako 1 místo 0..1. To je možné v případě, že třídy Řidič a Pasažér definujeme jako řidiče auta a pasažéra v autě, tedy tak, že nezahrnují řidiče a pasažéry jiných dopravních prostředků. Pak by vazba znamenala, že pasažér jede právě v jednom automobilu, anebo to není pasažér.
Shrnutí • • • •
Asociace je nejobecnějším vztahem mezi třídami a objekty. Asociace mezi třídami nevyjadřuje konkrétní akci ani činnost, ale existenci vztahu. Násobnost (kardinalita) vztahu určuje kolik objektů se vazby učastní. Reflexivní asociace je vztah mezi objekty téže třídy.
Pro úplnost je nezbytné dodat, že v praxi je většina vazeb označených nyní jako asociace v průběhu návrhu upřesněna do podoby agregace nebo kompozice. Předcházející text měl pouze za účel vysvětlit základní principy tvorby vazeb (násobnosti, popisování, reflexivita, atp.).
1
Vztahy Test Způsob vyhodnocení: Při vyhodnocení budou za nesprávné odpovědi strhnuty body.
1. Následujícímu diagramu odpovídá popis:
2
Vyberte jen jednu z následujících možných odpovědí.
a) Každý hlídač může hlídat více budov. Každou budovu má na starost právě jeden hlídač. Pes může chodit s více hlídači. Hlídač chodí s maximálně jedním psem. Ke každé budově náleží jeden nebo více vrátných. Vráný má na starost právě jednu budovu. b) Každý hlídač může hlídat více budov. Každou budovu má na starost právě jeden hlídač. Pes může chodit s více hlídači. Hlídač chodí s maximálně jedním psem. Ke každé budově náleží právě jeden vrátný. Vráný může mít na starost více budov. O každou budovu se stará právě jeden příslušný vrátný. c) Každý hlídač má na starost právě jednu budovu. Budovu může hlídat libovolný počet hlídačů. Pes náleží nejvýše jednomu hlídači. Hlídač může mít více psů. Ke každé budově náleží jeden nebo více vrátných. Vráný má na starost právě jednu budovu. 2. Následujícímu diagramu odpovídá popis:
Vyberte jen jednu z následujících možných odpovědí.
a) Každý doktor se stará právě o jednoho pacienta. O pacienta se může starat jeden nebo více doktorů. Každý doktor umí léčit alespoň jednu nemoc. Každá nemoc je léčena právě jedním doktorem. Doktorovi pomáhá nejvýše jedna sestra. Každá sestra pomáhá alepoň jednomu doktorovi.
2
2
Objektově orientované programování b) Každý doktor se stará alespoň o jednoho pacienta. O každého pacienta se stará právě jeden jeho doktor. Každý doktor umí léčit jednu určitou nemoc. Některé nemoci jsou léčeny více doktory. Doktorovi pomáhá alespoň jedna sestra. Každá sestra pomáhá nejvýše jednomu doktorovi. c) Každý doktor se stará alespoň o jednoho pacienta. O každého pacienta se stará právě jeden jeho doktor. Každý doktor umí léčit jednu určitou nemoc. Některé nemoci jsou léčeny více doktory. Doktorovi pomáhá nejvýše jedna sestra. Každá sestra pomáhá alepoň jednomu doktorovi.
3. Následujícímu diagramu odpovídá popis:
2
Vyberte jen jednu z následujících možných odpovědí.
a) Každý sportovec náleží právě do jednoho klubu. Do klubu může chodit libovolný počet sportovců. Každý sportovec má právě jednoho trenéra. Trenér se stará právě o jednoho sportovce. Každý klub má alespoň jednoho sponzora. Spozor financuje nejvýše jeden klub. b) Každý sportovec náleží právě do jednoho klubu. Do klubu může chodit libovolný počet sportovců. Každý sportovec má právě jednoho trenéra. Trenér se stará právě o jednoho sportovce. Každý klub má nejvýše jednoho sponzora. Spozor financuje alespoň jeden klub. c) Každý sportovec může chodit do libovolného počtu klubů. Ke každému klubu náleží jeho příslušný sportovec. Každý sportovec má právě jednoho trenéra. Trenér se stará právě o jednoho sportovce. Každý klub má nejvýše jednoho sponzora. Spozor financuje alepoň jeden klub.
Agregace
Silnější typy vazeb Agregace Agregace (zahrnutí, obsažení) je forma asociace, která znázorňuje skutečnost, že jeden objekt (agregovaný objekt) je částí druhého objektu (agregátu). Prakticky jsou všechny objekty složené z dílčích objektů, například tužka se skádá z tuhy a z obalu, tuha se dále skládá z molekul, molekuly se skládájí z atomů, atd. Třída je však pouze modelem skutečnosti a proto v ní definujeme další objekty pouze pokud je to účelné z hlediska modelu. Agregace a z ní vycházející kompozice nám umožňují zavést v modelu hierarchii, tj. všechny třídy nejsou na jedné úrovni, ale některé jsou částmi větších celků.
3
Vztahy
Definice třídy Auto s vlastnostmi: SPZ, barva, vykonMotoru, typMotoru, pocetValcu, palivo.
class Auto { public: string SPZ; string barva; int vykonMotoru; string typMotoru; int pocetValcu; string palivo; };
(V UML znaménko ”-” označuje private. Zde se UML a kód neshodují, jde však pouze ilustraci zápisu.) Nevýhodou této definice je ignorování přirozených vztahů mezi atributy (výkon motoru souvisí s typem motoru a nesouvisí s barvou auta). Je výhodnější použít strukturovanou definici objektu. Objekt z třídy Auto obsahuje (agreguje) objekt z třídy Motor. Třída Auto má atributy: barva, značka, počet náprav. Každý motor má výkon, počet válců a výrobce.
class Motor { public: int vykon; int pocetValcu; string vyrobce; };
class Auto { public: string barva; string znacka; int pocetNaprav; Motor motor; //atribut pro ulozeni objektu tridy Motor };
Diagram agregovaných tříd
Auto a Motor, agregace se značí prázdným kosočtvercem u agregátu.
4
Objektově orientované programování
Aby příklad byl úplný je nutné ukázat si, jak v praxi dojde k provázání dvou konrétních objektů. Uvažujme třídy z předchozího příkladu a vytvořme jejich instance, které následně propojíme pomocí agregace.
int main(){ Motor* tdi = new Motor; // vytvorime motor Auto* fiesta = new Auto;// vytvorime auto fiesta->motor = tdi; // provazeme motor s autem, v tomto okamziku se vytvorila vazba fiesta->motor->pocetValcu = 6; // nyni muzeme pristupovat k motoru tdi skrze odkaz // v instanci tridy auto tdi->pocetValcu = 6; // udela to same jako predchozi radek Motor* hdi = new Motor; // vytvorime jiny motor fiesta->motor = hdi; // soucasti auta bude ted jiny motor fiesta->motor->pocetValcu = 6; // nyni se nastavil pocet valcu u motoru HDI delete hdi; // smazani vsech objektu delete tdi; delete fiesta;
}
Agregace je využívána především pro zlepšení opakoveného použití částí zdrojového kódu. Po rozdělení konkrétní třídy na více menších a obecnějších tříd, je možné tyto dílčí vzory využívat při sestavování jiných a komplikovanějších. Například třídu Motor z příkladu 12 můžeme použít nejen v definici třídy Auto, ale i v definici libovolného jiného stroje, který má motor. Vhodným rozložením složité třídy na více agregovaných tříd se deklarace těchto tříd značně zjednodušší a zpřehlední.
Agregace je dočasná vazba mezi dvěma objekty. Objekty vzniknou nezávisle na sobě, pak jsou provázány (tato vazba může být kdykoliv zrušena nebo nahrazena jinou) a nakonec každý zvlášť zanikne. Např. ”motor” tedy není nedělitelnou součástí ”auta”. Může být libovolně měněn v průběhu běhu programu.
Kompozice Kompozice je specifickou formou agregace 13 , která nepovoluje agregovanému objektu existovat mimo agregát. Život části tedy začíná a končí s životem celku. Z hlediska deklarace se jedná o naprosto totožnou vazbu jako agregace (značí se plným kosočtvercem), rozdíl je pouze v implementaci zacházení s objektem.
12 13
javascript:void(0);/*1162929939046*/ a tedy i asociace, protože agregace je specifickou formou asociace.
5
Vztahy
Z hlediska obchodníka s automobily je vztah mezi třídou Motor a Auto spíše kompozice. Obchodník automobil koupí i s motorem (motor tedy vzniká současně s autem) a prodává ho i s motorem (motor tedy zaniká současně s autem), neprodává samostatné motory ani automobily bez motorů. Motor je z jeho pohledu tedy nedílnou součástí automobilu.
Vazba agregace bývá také nědy označována jako dvě vazby ”Part-of” (je částí) a ”Has-a” (má část).
Rozdíl implementace kompozice a agregace
V předchozím textu bylo ukázáno, jak je v praxi implementována agregace. Objekty byly vytvořeny nezávisle na sobě, provázány a nakonec každý zvlášť vymazány. V případě kompozice jsou všechny současti celku vytvořeny v konstruktoru celku, pak existují pevně spjaty s celkem a nakonec jsou smazány pomocí destruktoru celku, tedy v okamžiku, kdy celek zanikne.
6
Objektově orientované programování
Ukažme si kompozici opět na zdrojovém kódu v C++:
#include using namespace std; class Motor { public: int vykon; int pocetValcu; string vyrobce; }; class Auto { public: string barva; string znacka; int pocetNaprav; Motor* motor; //atribut pro ulozeni objektu tridy Motor Auto(){ // neobejdeme se bez konstruktoru motor = new Motor; // motor nyni ukazuje na nove vytvorenou instanci tridy Motor
} ~Auto(){ // musime mit i destruktor, ktery smaze motor delete motor;
} }; int main(){ Auto* fiesta = new Auto; // v tomto okamziku se vytvorilo auto a v nem i motor fiesta->motor->pocetValcu = 6; // korektni pristup k motoru delete fiesta; // ted se zrusilo i auto i motor return 0;
}
Kompozice je těsnější typ vazby. Objekty, které jsou součástmi celku jsou vytvářeny v kontruktoru celku a mazány v destruktoru celku. Jsou tedy na celku zcela závislé.
Jaký je rozdíl mezi agregací a kompozicí. Dokážete tento rozdíl popsat v jazyku C++?
7
Vztahy
Řešené příklady k procvičení problematiky 1. Motorové vozidlo
Definujte třídu Motor, která bude mít alespoň tři atributy popisující vlastnosti motoru. Tyto atributy budou nastavitelné pouze v konstruktoru a ke každému bude existovat metoda pro čtení getAtribut. Dále nadefinujte třídu Auto, která bude mít atributy popisující vlastnosti automobilu a bude v sobě agregovat objekt třídy Motor. Atributy třídy Auto nastavujte pomocí konstruktoru. Ve třídě Auto implementujte metodu vypisVlastnosti, která vypíše vlastnosti automobilu.
Třída Motor Název atributu
Hodnota atributu
Datový typ
Objem
1,4 l
číslo
Výkon
60 koní
číslo
Palivo
nafta
řetězec
Metody: getObjem, getVykon, getPalivo Třída Auto Název atributu
Hodnota atributu
Datový typ
Značka
Škoda
řetězec
Typ
Fabia
řetězec
Barva
Vyblitě zelená
řetězec
Počet dveří
5
číslo
Motor
MujMotor
Motor
Metody: vypisVlastnosti Diagram tříd:
8
Objektově orientované programování 2. Počítač a jeho komponenty
Definujte alespoň tři třídy popisující komponenty PC (např. procesor, RAM, monitor, I/O zařízení,. . . ). Dále nadefinujte třídu Pocitac, která bude obsahovat (agregovat) ostatní komponenty. Protože atributy komponent jsou neměnné, nastavujte je pomocí parametrů konstruktoru. Každá komponenta bude obsahovat metody typu getVlastnost. Tyto metody využijte v metodě Pocitac::PochlubSeJakJsesSkvelej, která vypíše všechny vlastnosti daného PC.
Třída Procesor Název atributu
Hodnota atributu
Datový typ
Výrobce
Intel
řetězec
Značka
Celeron
řetězec
Frekvence
1 000 MHz
číslo
Metody: getVyrobce, getZnacka, getFrekvence Třída RAM Název atributu
Hodnota atributu
Datový typ
Výrobce
KingMax
řetězec
Kapacita
256 MB
číslo
Metody: getVyrobce, getKapacita Třída IO Název atributu
Hodnota atributu
Datový typ
Monitor
ano
bool
Klávesnice
ano
bool
Myš
ano
bool
Reproduktory
ne
bool
Tiskárna
ano
bool
Scanner
ne
bool
Mikrofon
ne
bool
Metody: isMonitor, isKlavesnice, isMys, isReproduktory, isTiskarna, isScanner, isMikrofon, getIO Třída Pocitac Název atributu
Hodnota atributu
Datový typ
Výrobce
Tesco
řetězec
Procesor
Intel Celeron
Procesor
RAM
KingMax 256MB
RAM
IO
Tesco IO
IO
Metody: pochlubSeJakJsesSkvelej Diagram tříd:
9
Vztahy
3. Ovocný sad
Nadefinujte třídu Strom popisující ovocný strom - druh, rok výsadby a očekávaný výnos. Implementujte metodu stariStromu, která vypočítá stáří stromu v letech. Dále nadefinujte třídu Sad, která bude popisovat sad obsahující 100 stromů (uvažujte jako matici 10x10 – viz kapitola o datových typech). Třídu obohaťte o metody pro výpočet průměrného stáří stromu, určení nejstaršího stromu a celkového výnosu.
Třída Strom Název atributu
Hodnota atributu
Datový typ
Druh
švestka
řetězec
Rok výsadby
1983
číslo
Výnos
40 kg
číslo
Metody: stariStromu, getVynos, getDruh Třída Sad
10
Objektově orientované programování
Název atributu
Hodnota atributu
Majitel
Pepík
Stromy
Datový typ string Strom[10][10]
Metody: prumerneStariStromu, nejstarsiStrom, celkovyVynos Diagram tříd:
Dědičnost
Dědičnost
Dědičnost je jeden z klíčových nástrojů OOP, který umožňuje vytváření nových tříd (nový programových modulů) jen naprogramováním změn oproti třídě jiné. Tento způsob vývoje aplikací bývá nazývan diferenciální programování (programming by extension) a zásadně přispívá ke zrychlení vývoje aplikací.
11
Vztahy
Uvažujme následující neúplné deklarace tříd:
class NakladniAuto { private: string SPZ; string barva; int rokVyroby; date STK; int maxNaklad; int maxOsob; int pocetNaprav; public: ... };
class OsobniAuto { private: string SPZ; string barva; int rokVyroby; date STK; int maxOsob; int pocetDveri; public: ... };
Je patrné, že obě třídy mají množství společných atributů (a metod), mezi třídami tedy musí existovat nějaká vazba. Obě třídy reprezentují nějaký typ automobilu, reprezentují nějaký speciální typ automobilu. Můžeme tedy říci, že obě třídy jsou specializací třídy Automobil, kterou definujeme tak, aby obsa-
12
Objektově orientované programování Vztah dědičnost (inheritance) je možné rozdělit na dvě doplňkové vazby - specializaci (realization) a zobecnění (generalization). Potomek (osobní auto nebo nákladní auto) rodičovské třídy je speciálním případem rodičovské třídy (auto), naopak rodičovská třída je zobecněním svých potomků. Rodičovská třída obsahuje pouze prvky společné všem potomkům.
Dědění a modifikátory viditelnosti Popišme si nyní, co se stalo v předchozím příkladu, když jsme místo dvou kompletních definicí tříd (nádkladního a osobní auta), vytvořili obecné auto a následně jeho potomky, kteří postihovali pouze změny oproti obecnému autu. První věc, na kterou se musíme podívat jsou modifikátory viditelnosti. Tato problematika je poměrně dost závislá na zvoleném programovacím jazyku, já ji vysvětlím jazyku C++. V C++, jak již víte, existují 3 modifikátory viditelnosti - public, protected a private. V případě dědičnosti začneme používat ten prostřední, který zatím nebyl pořeba - protected. Jaký je význam protected? Z hlediska přístupu k atributům/metotodám je stejný jako private. Tedy:
K metodám nebo atributů označeným jako protected, nelze přistupovat z hlavní funkce programu nebo metod jiných tříd.
Nyní se podívejme, co se děje s atributy v případě dědění. Základní typ dědění u C++ je dědění způsobem public (toto označení typu dědění). V případě dědění tímto způsobem nastává následující situace: • Atributy a metody, které jsou v předkovi označeny public se ”jakoby” zkopírují do potomka a lze je v potomkovi využívat (např. metodu vratRokVyroby, lze klidně volat v jiných metodách tříd OsobniAuto i NakladniAuto i z hlavní funkce programu). • Atributy a metody, které jsou v předkovi označeny protected se ”jakoby” zkopírují do potomka a lze je v potomkovi využívat (např. atribut rokVyroby, lze modifikovat v jiných metodách tříd OsobniAuto i NakladniAuto, ne však i z hlavní funkce programu - vzpomeňte, chová se to z hlediska přístupu jako private). • Atributy a metody, které jsou v předkovi označeny private nelze je v potomkovi využívat. Pokud něco v předkovi takto označíte, potomek to ”neuvidí”. Nicméně, přesto bude takový např. atribut existovovat a pokud bude mít předek i např. public metodu, která ho umožní nastavit, potomek bude moci tuto metodu zavolat a do atributu zapsat.
Kromě dědění způsobem public, které bylo právě popsáno, existují v C++ i dědění způsobem protected a private. V zásadě se jedná o podobný princip, pouze se částečně mení modifikátory viditelnosti zděděných atributů a metod. Metody dědění a rozdíly mezi nimi ilustruje přehledně následující tabulka. Tab. 13: Metody dědění v C++ V předkovi je public Dědíme způsobem public Dědíme způsobem protected Dědíme způsobem private
V potomkovi je public
V předkovi je protected V potomkovi je protected
V předkovi je private V potomkovi není přístupný
V potomkovi je protected
V potomkovi je protected
V potomkovi není přístupný
V potomkovi je private
V potomkovi je private
V potomkovi není přístupný
13
Vztahy
Dědění umožňuje snížení redundance kódu a vícenasobné používání již hotových funkčních celků. Pomocí dědění je tedy možné snadno sdílet hotový kód a jemně upravovat jeho funkci. Pomocí agregace je také možné sdílet již hotové třídy, ale nelze dále měnit jejich funkci. Jak při použití dědičnosti, tak při použití agregace se funkčnost části kódu (tedy nějaké metody) změní jejím obalením novou metodou, která před nebo po zavolání původní metody něco udělá. Při použití agregace však je nová (obalující) metoda součástí agregátu a nelze ji tedy dále využít u jiného agregátu, tedy nová metoda není součástí agregovaného objektu. Použije-li se však dědičnost, pak je možné novou (obalující) metodu umístit do nového objektu, který je dále možné použít stejně jako objekt původní.
Při vytváření zdědných objektů je třeba dbát na to, aby změny byly malé. a Hrozí totiž nebezpečí, že mezi potomkem a rodičem vznikne silná závislost, tedy že potomek závisí na implemenetaci rodiče, což ruší výhody dědičnosti, protože pak je nutné při změně rodiče opravovat i potomky. a
Malou změnou se myslí malá změna funkčnosti metody, nesouvisí to přímo s počtem řádků.
Je potřeba dávat velký pozor na to, jestli není možné nahradit dědičnost jiným typem vazby (např. agregací). Dědičnost je nejsilnějí vazba a nelze ji po celou dobu běhu programu změnit.
Přetížení a překrytí metody
Při vytváření potomka můžeme modifikovat již exitující metody předka. V praxi máme tři možnosti: 1. Ponecháme metodu předka nezměněnou. 2. Vytvoříme v potomkovi metodu, která se jmenuje stejně jako v předkovi a má i stejný počet parametrů - překrytí. 3. Vytvoříme v potomkovi metodu (nebo i v předkovi), která se jmenuje stejně jako jiná, ale má jiný počet nebo typy parametrů - přetížení.
Pokud použijeme překrytí a vytvoříme instanci potomka, potom se implicitně používá ”verze” metody z potomka, která překryla původní. Pokud bychom v takovém případě chtěli explicitně zavolat verzi z předka, musíme to překladači říci např. pomocí předpony identifikující předka (Predek::vypis info()). V případě přetížené metody je situace jednoduchá, protože metody se liší počtem parametrů. Stačí tedy metodu zavolat se správným počtem nebo typy parametrů a překladač sám příslušnou metodu identifikuje.
Chování konstruktorů a destruktorů v případě dědičnosti Chování konstruktorů a destruktorů může být ze začátku trochu matoucí. Platí však několik jasných pravidel, které si stačí osvojit.
14
Objektově orientované programování
1. Konstruktory se volají v pořadí od ”nejstaršího” předka po aktuální třídu. 2. Destruktory se volají v opačném pořadí – od aktuální třídy po nejstaršího předka 3. Pokud používáme parametrické konstruktory, je nutné volat ručně parametrické konstruktory předků (viz příklad níže). 4. Pokud má předek pouze paramterický konstruktor, musíme ho ručně zavolat, pokud má i neparamerický konstruktor, nemusíme volat nic a automaticky se zavolá onen bezparametrický. 5. Konstruktor potomka si musí vyžádat i parametry nutné pro vytvoření předka.
Aby byly lépe vysvětleny bod 3 a 4, vytvořme si třídu Vozidlo s vlastností maximalniRychlost, která je zadána př vytvoření vozidla a jejího potomka Motocykl. Motocykl má vlastnost značka, která je vynucena konstruktorem.
class Vozidlo{ private: int maximalniRychlost; public: Vozidlo(int zadanaMaxRychlost){ maximalniRychlost = zadanaMaxRychlost;
} }; class Motocykl: public Vozidlo{ private: string znacka; public: // konstruktor motocyklu vynuti zadani parametru co sam potrebuje // i co potrebuji predci. O rychlost se postara kontruktor predka. Motocykl(int maxRychlost, string zadanaZnacka):Vozidlo(maxRychlost){ // konstruktor motocyklu resi jen znacku znacka = zadanaZnacka;
} };
Řešené příklady k procvičení problematiky dědičnosti 1. Jméno a příjmení
Definujte třídu Jmeno, která obsahuje pouze textový řetězec se jménem a metody pro jeho nastavení, zjištění a vypsání na obrazovku. Dále definujte třídu JmenoAPrijmeni, která je potomkem třídy Jmeno a která navíc obsahuje atribut příjmení a metody pro jeho nastavení a zjištění. Metodu pro vypsání jména na obrazovku upravte tak, aby vypisovala jméno i příjmení. Nejprve vytvořte UML model a teprve potom pište kód v C++. Můžete vytvořit ještě dalšího potomka, který bude třídu rozšiřovat ještě o prostřední jméno a umožní tak zápis jmen typu „Vladimír Iljič Lenin“.
15
Vztahy
Třída Jmeno: Název atributu
Hodnota atributu
Datový typ
Jméno
„Pepíček”
řetězec
Metody: setJmeno, getJmeno, vypisJmeno Třída JmenoAPrijmeni: Název atributu
Hodnota atributu
Jméno
„Pepíček”
Datový typ řetězec
„Novák”
řetězec
Příjmení
Metody: setJmeno, getJmeno, vypisJmeno, setPrijmeni, getPrijmeni 2. Vozidlo
Vzpomínáte na třídu Auto agregující třídu Motor? Nyní vytvoříme dva potomky této třídy - OsobniAuto a NakladniAuto obsahující relevantní atributy (u osobního automobilu to bude počet osob, u nákladního nosnost). Nejprve vymodelujte pomocí UML, teprve poté pište kód v C++!
Třída Motor
16
Objektově orientované programování
Název atributu
Hodnota atributu
Datový typ
Objem
1,4 l
číslo
Výkon
60 koní
číslo
Palivo
nafta
řetězec
Metody: getObjem, getVykon, getPalivo Třída Auto Název atributu
Hodnota atributu
Datový typ
Značka
Škoda
řetězec
Typ
Fabia
řetězec
Barva
Vyblitě zelená
řetězec
Počet dveří
5
číslo
Motor
MujMotor
TMotor
Metody: vypisVlastnosti Třída OsobniAuto Název atributu
Hodnota atributu
Datový typ
Značka
Škoda
řetězec
Typ
Fabia
řetězec
Barva
Vyblitě zelená
řetězec
Počet dveří
5
číslo
Motor
MujMotor
TMotor
Počet osob
5
číslo
Metody: vypisVlastnosti, setPocetOsob, getPocetOsob Třída NakladniAuto Název atributu
Hodnota atributu
Datový typ
Značka
Škoda
řetězec
Typ
Fabia
řetězec
Barva
Vyblitě zelená
řetězec
Počet dveří
5
číslo
Motor
MujMotor
TMotor
Nosnost
8 tun
číslo
Metody: vypisVlastnosti, setNosnost, getNosnost Digram tříd
17
Vztahy
3. Bankovní účet
Nadefinujte třídu Ucet, která bude implementovat nejzákladnější operace s bankovním účtem: Vklad, výběr a zjištění stavu. Dále nadefinujte potomka této třídy: UrocenyUcet, která bude navíc implementovat metodu pro připsání stanoveného úroku. Nakonec nadefinujte třídu UrocenyUcetSPoplatkem, která bude potomkem úročeného účtu a bude navíc implementovat strhnutí poplatku z účtu. Nejprve vymodelujte pomocí UML, teprve poté pište kód v C++!
Třída Ucet Název atributu
Hodnota atributu
Datový typ
Stav konta
5,50 Kč
číslo
Metody: Vklad, Vyber, getStav Třída UrocenyUcet Název atributu
Hodnota atributu
Datový typ
Stav konta
5,50 Kč
číslo
Úroková sazba
0,5 %
číslo
Metody: Vklad, Vyber, getStav, getUrokovaSazba, setUrokovaSazba, PrictiUrok Třída UrocenyUcetSPoplatkem Název atributu
Hodnota atributu
Datový typ
Stav konta
5,50 Kč
číslo
Úrok
0,5 %
číslo
Poplatek
30 Kč
číslo
18
Objektově orientované programování Metody: Vklad, Vyber, getStav, getUrokovaSazba, setUrokovaSazba, PrictiUrok, setPoplatek, getPoplatek, StrhniPoplatek
4. Zaměstnanec
Nadefinujte třídu Clovek obsahující jako atributy základní údaje o člověku (jméno, příjmení, datum narození. . . ) a metody pro jejich zjištění a nastavení. Pomocí dědění tuto třídu rozšiřte na Zamestnanec - třídu, která navíc bude obsahovat informace o pracovní pozici a platu zaměstnance. Nejprve vymodelujte pomocí UML, teprve poté pište kód v C++! Chcete-li si procvičit více, pro jméno a příjmení člověka agregujte třídu JmenoAPrijmeni z příkladu uvedeného dříve.
5. ZOO
Nadefinujte třídy Zivocich obsahující jako atributy nějaké smyslupné informace o živočichovi. Dále nadefinujte potomky Savec, Ptak, které budou vhodně rozšiřovat třídu Zivocich. Dále nadefinujte alespoň dva potomky každého z potomků (např. Kocka, Pes, Sykorka, Vrana,. . . ). Vyzkoušejte volání metod předků, specifikátory dědičnosti, konstruktory, destruktory. . .
1
Vztahy Test Způsob vyhodnocení: Při vyhodnocení budou za nesprávné odpovědi strhnuty body.
1. Do kterých atributů můžeme zapisovat z metod třídy Predek? class Predek{ public: int a; protected: int b; private: int c; }; class Potomek : public Predek{ public: int d; protected: int e; private: int f; };
1
Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d) e) f)
c f e b d a
2. Do kterých atributů z třídy Predek můžeme zapisovat z metod třídy Potomek? class Predek{ public: int a; protected: int b; private: int c; }; class Potomek : public Predek{ public: int d; protected: int e; private: int f; };
1
Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b b) a c) c 3. Co se vypíše na obrazovku? class Predek{ public: void vypisIformace(){ cout << "Ahoj, ja jsem predek" << endl;
}
1
2
Objektově orientované programování
}; class Potomek : public Predek{ public: void vypisIformace(){ cout << "Ahoj, ja jsem potomek" << endl;
} }; int main(){ Predek* instance = new Predek; instance->vypisInformace(); ...
} Vyberte jen jednu z následujících možných odpovědí.
a) Nic z uvedeného, metoda se stejnou hlavičkou v předkovi i v potomkovi nemůže existovat. b) Ahoj, ja jsem potomek c) Ahoj, ja jsem predek 4. Co se vypíše na obrazovku? class Predek{ public: void vypisIformace(){ cout << "Ahoj, ja jsem predek" << endl;
1
} }; class Potomek : public Predek{ public: void vypisIformace(){ cout << "Ahoj, ja jsem potomek" << endl;
} }; int main(){ Potomek* instance = new Potomek; instance->vypisInformace(); ...
} Vyberte jen jednu z následujících možných odpovědí.
a) Nic z uvedeného, metoda se stejnou hlavičkou v předkovi i v potomkovi nemůže existovat. b) Ahoj, ja jsem potomek c) Ahoj, ja jsem predek 5. Do kterých atributů můžeme zapisovat z hlavní funkce programu a jiných metod jiných tříd pokud vytvoříme instanci třídy Potomek? class Predek{ public: int a; protected: int b; private: int c; }; class Potomek : public Predek{ public: int d; protected: int e; private: int f; };
1
3
Vztahy
Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d) e) f)
e f d b a c
Příklady
Příklad 1 Udělejte třídu Člověk. Člověk má výšku a váhu. Každý člověk je složený objekt (kompozice), který má Mozek s počtem šedivých a bílých buněk a Srdce. Srdce má tep a tlak. Když vytvoříme nového člověka, musí vzniknout i jeho mozek a srdce. Naopak - při jeho ”smrti”, musí být zrušeny i jeho orgány. Zkuste vytvořit par. konstruktor člověka, který vynutí zadaní parametrů orgánů. Řešení:
/* V tomto prikladu zamerne pomijime zapouzdreni. V praxi by byl takovy navrh nevhodny, ale zavedeni soukromych atributu by cely problem komplikovalo a castecne zamlzilo vysvetlovanou latku - kompozici. */ #include using namespace std; class Mozek{ public: int pocetSedivychBunek; int pocetBilychBunek; Mozek(int zadaneSedive, int zadaneBile){ pocetBilychBunek = zadaneBile; pocetSedivychBunek = zadaneSedive;
} }; class Srdce{ public: int tep; int tlak; Srdce(int zadanyTep, int zadanyTlak){ tep = zadanyTep; tlak = zadanyTlak;
} }; class Clovek{ public: int vyska; int vaha; Mozek* mozecek; Srdce* srdicko; Clovek(int vyska, int vaha, int bile, int sedive, int tep, int tlak){ this->vyska = vyska; // Pro pripomenuti, jak pristoupit k prekrytemu this->vaha = vaha; // atributu. mozecek = new Mozek(bile, sedive); srdicko = new Srdce(tep, tlak);
} ~Clovek(){ delete(srdicko);
4
Objektově orientované programování
delete(mozecek);
} }; int main (int argc, char * const argv[]) { Clovek* pepik = new Clovek(178, 68, 200000, 300000, 50, 90); cout << "Vyska Pepika: " << pepik->vyska << endl; cout << "Tlak Pepika: " << pepik->srdicko->tlak << endl; cout << "Mozek Pepika: " << pepik->mozecek->pocetBilychBunek << ", " << pepik->mozecek->pocetSedivychBunek << endl; delete(pepik); return 0;
}
Příklad 2 Navrhněte a implementujte jednoduchý inf. systém autodílny. Eviduje techniky (jejich jména), faktury (jakou zakázku pokrývají a jaký zákazním je platí, kolik platí, jestli už je zaplacena), zákazníky (jméno) a zakázky (popis zakázky a jaký zákazník je objednal). Faktura bude mít metodu ”vypiš fakturu”, která vypíše informace o faktuře, metodu, která nastaví proplaceno na true a metodu na opravu částky. Technik bude mít metodu na zpracování zakázky, které se předá v parametru zpracovávaná zakázka a návratovou hodnotou bude instance třídy faktura. Řešení:
#include using namespace std; class Zakaznik{ public: string jmeno; Zakaznik(string jmeno){ this->jmeno = jmeno;
} }; class Zakazka{ public: Zakaznik* zakaznik; string popis; Zakazka(Zakaznik* zakaznik){ this->zakaznik = zakaznik;
} }; class Faktura{ public: Zakazka* zakazka; Zakaznik* zakaznik; private: int castka; bool zaplaceno; string technik; public: Faktura(int castka, Zakazka* zakazka, string technik){ this->castka = castka; this->zakazka = zakazka; this->zakaznik = zakazka->zakaznik; this->technik = technik;
} void opravCastku(int castka){ this->castka = castka;
}
5
Vztahy
void proplat(){ zaplaceno = true;
} void vypisFakturu(){ cout << "Jmeno: " << zakaznik->jmeno << endl; cout << "Popis prace: " << zakazka->popis << endl; cout << "Kdo praci delal: " << technik << endl; cout << "Cena: " << castka << endl;
} }; class Technik{ public: string jmeno; Technik(string jmeno){ this->jmeno = jmeno;
} Faktura* zpracujZakazku(Zakazka* zakazka){ // zde by se spocitala cena, provedly dalsi operace // tj. volame metody jako spocti naklady... int cenaOpravy = 1000; Faktura* faktura = new Faktura(cenaOpravy, zakazka, jmeno); return faktura;
} }; int main(){ Technik* pepik = new Technik("Pepik Kosik"); Zakaznik* novak = new Zakaznik("Jiri Novak"); Zakazka* opravaAuta = new Zakazka(novak); opravaAuta->popis = "Oprava nabourane Skody Felicia"; Faktura* novaFaktura = pepik->zpracujZakazku(opravaAuta); novaFaktura->vypisFakturu(); //cout << opravaAuta->faktura->vypisFakturu(); delete(novak); delete(opravaAuta); delete(novaFaktura); delete(pepik); return 0;
}
Příklad 3 Vytvořte model souborového systému, který se skládá ze souborů a adresářů. Adresář může mít rodiče. Soubor musí být umístěn v určitém adresáři. Jak soubor, tak adresář mají své jméno. Řešení:
#include using namespace std; class Adresar{ private: string jmeno; public: Adresar* rodic; // pozor agregace // predavam odkaz v konstruktoru, ale stale se jedna // o agregaci, neplest s asociaci, tam ten odkaz zanika
6
Objektově orientované programování
// po konci metody Adresar(string jmeno, Adresar* rodic){ this->jmeno = jmeno; this->rodic = rodic;
} string vratJmeno(){ return jmeno;
} }; class Soubor{ private: string jmeno; public: Adresar* ulozen; // agregace Soubor(string jmeno, Adresar* ulozen){ this->jmeno = jmeno; this->ulozen = ulozen;
} string vratJmeno(){ return jmeno;
} }; int main (int argc, char * const argv[]) { Adresar* root = new Adresar("root", NULL); Adresar* dokumenty = new Adresar("dokumenty", root); Soubor* dopis = new Soubor("dopis.txt", dokumenty); // priklady vyuziti agregace pro vypis informaci cout << "Soubor " << dopis->vratJmeno(); cout << " je ulozen v adresari " << dopis->ulozen->vratJmeno(); cout << " a ten je ulozen v adresari " << dopis->ulozen->rodic->vratJmeno(); cout << endl; // zde si vyzkousejte ruzne varianty vypisu jmen { prime, neprime... // v pripade casu i asociaci, kdy treba rodic adresar, vypise jmeno potomka // na ktereho je mu predan odkaz { vypisJmenoPotomka(Adresar* potomek) delete(dopis); delete(dokumenty); delete(root); return 0;
}
Příklad 4 Prvňáček používá sešit a kalkulačku. Definujte třídu Prvnacek, která obsahuje metody napisDoSesitu() a vypocitej(), třídu Sesit, která obsahuje metodu pripis() a třídu Kalkulacka, která obsahuje metody plus() a minus(). Implementujte hlavní program demonstrující zasílání zpráv mezi prvňáčkem, sešitem a kalkulačkou.
Vztahy Řešení: Diagram tříd:
Mezi třídami je jen obyčejná vazba asociace, prvňáček může zapisovat do libovolného sešitu (tedy nemusí to být jeho sešit), v jednom okamžiku však nemůže zapisovat do více sešitů. Vazba mezi prvňáčkem a kalkulačkou je obdobná. Mezi kalkulačkou a sešitem přímá vazba není. Násobnosti ve všech uzlech by bylo možné zapsat také jako 0..1. Zdrojový kód programu v C++:
#include using namespace std; class Sesit { //deklarace třídy Sesit private: string text; /*cely text sešitu, atribut text je pouze pro čtení, je tedy definovaná jen metoda getText() */ public: Sesit(); //konstruktor void pripis(string aText); /* metoda pripis není typická metoda pro nastavení atributu text, proto se nejmenuje setText(). */ string getText(); }; class Kalkulacka { //deklarace třídy Kalkukacka private: int pamet; public: Kalkulacka(); int plus(int operand1, int operand2); int minus(int operand1, int operand2); }; class Prvnacek { //třída Prvnacek nemá žádné atributy public: void napisDoSesitu(Sesit* kam, string co); int vypocitej(Kalkulacka* kde, int operand1, int operand2, string operace); }; //============= implementace tridy sesit Sesit::Sesit() { text = "";
} void Sesit::pripis(string aText) { text = text + aText;
} string Sesit::getText() { return text;
} //============= implementace tridy kalkulacka Kalkulacka::Kalkulacka() { pamet = 0;
}
7
8
Objektově orientované programování
int Kalkulacka::plus(int operand1, int operand2) { return operand1 + operand2;
} int Kalkulacka::minus(int operand1, int operand2) { return operand1 + operand2;
} //============= implementace tridy prvnacek void Prvnacek::napisDoSesitu(Sesit* kam, string co) { kam->pripis(co);
} int Prvnacek::vypocitej(Kalkulacka* kde, int operand1, int operand2, string operace)
{ if (operace == "+") { return kde->plus(operand1, operand2);
} if (operace == "-") { return kde->minus(operand1, operand2);
} } //============= hlavni funkce programu int main(int argc, char *argv[]) { //deklarace promennych Prvnacek* tonda; Sesit* tonduvSesit; Sesit* jirkuvSesit; Kalkulacka* tondovaKalkulacka; //vytvoreni objektu tonda = new Prvnacek(); tonduvSesit = new Sesit(); jirkuvSesit = new Sesit(); tondovaKalkulacka = new Kalkulacka(); tonda->napisDoSesitu(tonduvSesit, " Ahoj! "); tonda->napisDoSesitu(tonduvSesit, " Ahoj! "); cout << "\nV sesite ma Tonda napsano: " << tonduvSesit->getText(); cout << "\nTonda spocital, ze 1 + 1 = " << tonda->vypocitej(tondovaKalkulacka, 1, 1, "+"); cout << "\nJirka ma v sesite napsano: " << jirkuvSesit->getText(); return EXIT SUCCESS;
}
Příklad 5 Definujte třídu Clovek, která popisuje člověka (jméno, příjmení, věk, pohlaví, plat, . . . ). Poté definujte třídu Firma, která je modelem firmy (ředitel, sekretářka, účetní, uklizečka, . . . ), a agreguje objekty třídy Clovek. Dále nadefinujte třídu Rodina, která je modelem rodiny (otec, matka, syn, dcera, . . . ). Třída rodina také agreguje objekty třídy člověk. Objekt třídy člověk, tedy může být agregován více objekty. Sestrojte UML diagram tříd.
Příklad 6 Definujte alespoň tři třídy, které popisují různá domácí zvířata. Definujte třídu Dvorek, na němž mohou být různá domácí zvířata. Vytvořte alespoň tři instance třídy Dvorek (tj. tři objekty typu Dvorek) s různými zvířaty. Vytvořte UML diagram tříd a program, který třídy implementuje.
Příklad 7 Definujte třídy popisující vybavení kanceláře (stůl, židle, lampa, počítač, . . . ). Je vhodné využít tříd definovaných i v jiných příkladech dříve. Dále definujte třídu Kancelar, která obsahuje atributy, které popisují vlastnosti kanceláře (klimatizace, počet oken, barvu stěn. . . ).
Pokročilé techniky OON Třída Kancelar agreguje objekty vybavení kanceláře. Sestavte diagram tříd a zapište deklaraci tříd v C++.
Příklad 8 Definujte třídu Vektor, která bude mplementovat dvourozměrný vektor. Atributy třídy budou souřadnice vektoru x a y. Třída bude obsahovat metody pro nastavení a zjištění souřadnic a výpočet délky vektoru. Vytvořte třídu VektorSOperacemim, která bude implementovat základní operace s vektorem, tedy násobení reálným číslem a skalární součin dvou vektorů. Pokud zrovna nemáte co na práci, můžete naprogramovat i vektorový součin. Nejprve vytvořte UML model, potom pište kód v C++.
2.2
Pokročilé techniky OON
Složitější případy dědičnosti a polymorfismus
Polymorfismus Polymorfismus bývá označován jako ”pozdní vazba”. Abychom se mohli bavit o pozdní vazbě, musíme nejdřív specifikovat, co je to ”časná vazba”. Až do kapitoly dědičnost jsme zatím používali časnou vazbu - překladač v době překladu vždy přesně věděl, která metoda se bude volat. Tedy vytvořili jsme objekt, zavolali jeho metodu.
Příklad časné vazby:
class Predek { public: void casna() { cout << "Metoda tridy Predek volana casnou vazbou" << endl; } }; class Potomek : public Predek { public: void casna() { cout << "Metoda tridy Potomek volana casnou vazbou" << endl; } }; int main(void)
{ Predek *n = new Predek; Potomek *p = new Potomek; n->casna(); // zavola se metoda Predka p->casna(); // zavola se metoda Potomka delete(n); delete(p); return 0;
}
Pozdní vazba se nám objevuje v souvislosti s děděním. Platí nám vlastnost, že potomek je vždy specializovanou verzí předka. Potomek je v určitém slova smyslu ”kompatibilní” s předkem. Tuto vlastnost využíváme takovým způsobem, že pokud vytvoříme odkaz na předka, můžeme tento odkaz ”naplnit” instancí potomka. Například v případě agregace. Jedna třída obsahuje odkaz na instanci třídy Příkaz. Třída Příkaz má potomky PříkazVykresnení, PříkazVypsání. Všichni implementují metodu proved(). Bez nutnosti přetypovávání nebo jiných nástrojů můžeme vytvořit instanci třídy PříkazVykresnení a odkaz na Příkaz nastavit na něj.
9
10
Objektově orientované programování Je evidentní, že překladač nemůže tušit, jakou instanci do tohoto odkazu dosadíme v průběhu běhu programu. Jednou to může být instance PříkazVykresnení, jednou PříkazVypsání. Překladač tedy neví, jaká implementace metody proved() se bude vykonávat, pokud ji voláme skrze odkazu na předka (Pokud bude dosazen PříkazVypsání, pak se zavolá PříkazVypsání::vykonej(), pokud PříkazVykresnení, tak se zavolá PříkazVykresnení::vykonej()). Toto chování nazýváme polymorfismem. Pokud chceme v programu při volání určité funkce využívat pozdní vazby, musíme překladači říci, aby kontroloval, jaká instance bude do odkazu reálně dosazena. To lze realizovat pomocí klíčového slova virtual. Celý problém je vysvětlen na následujícím příkladu.
11
Pokročilé techniky OON
Příklad nekorektní pozdní vazby:
class Predek { public: void pozdni() { cout << "Nadtrida" << endl; } }; class Potomek : public Predek { public: void pozdni() { cout << "Podtrida" << endl; } }; int main(void) { Predek* problem; // deklarujeme, ze problem bude ukazovat na Predka problem = new Potomek; // na místo předka dosadíme potomka problem->pozdni(); // vola se metoda Predka, misto aby se zavolala metoda potomka // pokud by predek metodu pozdni neobsahoval nastal by chyba delete problem; return 0;
} Příklad korektní pozdní vazby na předchozím problému:
class Predek { public: // klicove slovo virtual rika "pockej si, na to, co se tam dosadi za objekt" virtual void pozdni() { cout << "Nadtrida" << endl; } }; class Potomek : public Predek { public: void pozdni() { cout << "Podtrida" << endl; } }; class Potomek2 : public Predek { public: void pozdni() { cout << "Podtrida" << endl; } }; int main(void) { Predek* problem; // deklarujeme, ze problem bude ukazovat na Predka problem = new Potomek; // na místo předka dosadíme potomka problem->pozdni();
// vola se metoda Potomka
problem = new Potomek2; problem->pozdni(); // vola se metoda Potomka2 delete problem; return 0;
}
Čistě virtuální metody a abstraktní třídy Mohli jste si všimnout, že v posledním příkladu bylo u deklarace metody použito klíčové slovo virtual. Tuto metodu jsme označili za virtuální. Takovou metodu často v potomcích
12
Objektově orientované programování překrýváme. Speciálním případem je čistě virtuální metoda. Je to typ metody, která nemá v předkovi žádnou implementaci (v C++ ji zapisujeme ”virtual navratovy typ metoda()=0;” ). Jelikož nemá tato metoda implementaci, je nezbytné, aby všichni potomci tuto implementaci obsahovali. Jinými slovy, nemůže existovat potomek takovéto třídy, který implementaci této metody neobsahuje. Třídě, které alespoň jednu takovouto metodu obsahuje říkáme abstraktní třída. Od takové třídy nelze vytvářet instance a slouží pouze pro dědění.
Příklad čistě virtuální třídy Auto a jejího potomka OsobniAuto:
// abstraktni trida s ciste virtualni metodou class Auto{ public: virtual void kdoJsem()=0; }; // potomek od ktere lze instance odvozovat class OsobniAuto : public Auto{ public: void kdoJsem(){ cout << "Jsem osobak" << endl;
} }; // chybny potomek - neobsahuje implementaci metody kdoJsem() class NakladniAuto : public Auto{ public: void jaJsem(){ cout << "Jsem nakladak" << endl;
} };
Vícenásobná dědičnost Vícenásobná dědičnost umožnuje jednu třídu odvodit od více tříd současně. Tedy jedna třída může být přímým následníkem více rodičovských tříd. Vícenásobnou dědičnost je možné využít pro další snížení duplicity zdrojového kódu, pokud dvě vzájemně nesouvisejicí (nebo jen vzdáleně související) třídy obsahují části kódu které potřebujeme využít v nové třídě. Vícenásobná dědičnost však přináší také řadu obtíží a proto není podporována ve všech objektových jazycích.
Syntaxe zápisu vícenásobné dědičnosti:
class Odvozená : přístup Základní1, přístup Základní2 ... { ... }; Syntaxe volání konstruktorů předků:
class Odvozená { Odvozená(param1, param2, ...) : Základní1(...), Základní2(...) { ... //tělo konstruktoru
} }
13
Pokročilé techniky OON
Příklady Kůň, pták a pegas
Definujte třídu Kun a třídu Ptak. Třídy budou mít konstruktor s povinným parametrem jmeno, který bude nastavovat atribut jmeno. Konstruktory a destruktory budou vypisovat na obrazovku zprávu, která bude obsahovat typ objektu (kůň nebo pták), podle ní bude možno konstruktory a destruktory identifikovat. Obě třídy budou mít metodu kdoJsem(), která vypíše atribut jmeno daného objektu. Třída Ptak bude mít navíc metodu let(), která bude vypisovat inaci o objektu a o prováděné akci (let), obdobně ve třídě Kun bude metoda klusej(). Definujte třídu Pegas, která dědí pomocí vícenásobného dědění z tříd Kun a Ptak. Konstruktor a destruktor třídy vypíše inaci o svém zavolání. Třída bude mít metodu pobyhujSe(typPohybu), která podle zadaného argumentu (např. výčtový typ) zavolá rodičovské metody klusej() nebo let(). Třída Pegas obsahuje metodu kdojsem(), která vypisuje jmena Pegase a jména rodičů. Úkoly: • V těle hlavního porgramu vytvořte instanci třídy Pegas se jménem Sedovlas a jmény rodičů Noh a Semik a zavolejte metodu kdojsem(). • Zkontrolujte vypisování inací o třídách v rámci konstruktorů a destruktorů a metody kdojsem(). • Navrhněte, co by se muselo změnit, aby se destruktor třídy Kun volal dříve nebo později než desktruktor třídy Ptak • Zavolejte metodu pohybuj se(...). • Upravte definici konstruktoru třídy Kun tak, aby bylo možné nastavovat atribut pocetZubu při vytváření objektu. • Navrhněte úpravu zdrojového kódu, která umožní přistupovat k atributu pocetZubu.
class Ptak Název atributu/metody()
Datový typ
Specifikátor
jmeno
string
protected
delka zobaku
int
protected
kdojsem()
void
public
let()
void
public
Ptak(string)
-
public
˜Ptak()
-
public
class Kun Název atributu/metody()
Datový typ
Specifikátor
jmeno
string
protected
pocet zubu
int
protected
Klusej()
void
public
kdojsem()
void
public
Kun(string)
-
public
˜Kun()
-
public
14
Objektově orientované programování
class Pegas Název atributu/metody()
Datový typ
Specifikátor
jmeno
string
private
kdojsem()
void
public
pohybuj se(t pohyb)
void
public
Pegas(string, string, string)
-
public
˜Pegas()
-
public
• Konflikty jmen: Při vícenásobné dědičnosti může nastat situace, že třída dědí z nadtříd, které mají stejné názvy atributů, nebo metod. - Musí se adresovat přes operátor :: . Např: Kun::jmeno, nebo Ptak::kdojsem(). - Odkaz na pocet zubu ve třídě Pegas vyvolá chybu.
15
Pokročilé techniky OON Rádio
Nadefinujte třídy Vysílač a Přijímač. Vysílač bude pro nás znamenat přístroj, který vysílá signál na nějaké frekvenci. Analogicky, přijímač je přístroj, který přijímá signál na nějaké frekvenci. Vysílač má atribut vysilaciFrekvence a přijímač prijimaciFrekvence typu float. Dále pak obě třídy mají atribut aktivni typu bool. Konstruktory obou tříd nemají parametry, vypisují inace o svém vzniku a nastavují atributy vysilaciFrekvence a prijimaciFrekvence na nějakou pevnou hodnotu. Vysílač má metodu vysilej která nemá argument a frekvence vysílání je nastavena dle atributu frekvence. Metoda nastavuje atribut aktivní na true. Další metodou je metoda ukonciVysilani, která nastavuje aktivni na false a vypisuje odpovídající hlášku. Informaci, zda vysílač vysílá (atrubut aktivni je nastaven na true) podává metoda jeAktivni(). Přijímač je konstruován analogicky. Vysílač a přijímač mají metodu ukonci(), která ukončuje spojení: nastaví atribut aktivni na false a vypisuje odpovídající hlášku. Nadefinujte objekt Rádio, který zdědí z objektů Přijímač a Vysílač. Rádio má atribut frekvence typu float, terý se nastavuje v konstruktoru. Konstruktor vypisuje hlášku, že se jedná o rádio na dané frekvenci. Dále má třída metodu ustav spojeni. Ta volá metody vysilej a prijimej a poskytuje jim jako argument svuj atribut frekvence. Všechny tři třídy vypisují ve svých destruktorech inační hlášky. Úkoly: • Deklarujte oběkt třídy Rádio. • Navažte spojení a ukončete spojení. • Přetěžte metodu vysilej ze třídy Vysilac tak, že bude přebírat argument frekvence typu float. Metoda nastavuje atribut třídy vysilaciFrekvence dle příchozího argumentu, nastavuje atribut aktivni na true a vypisuje příslušnou inační hlášku. • To stejné proveďtě pro třídu Prijimac. • Doplňte do destruktorů tříd Vysilac a Prijimac kód tak, aby v případě kdy tělo programu nezavolá metodu Radio::ukonci() (tj. Metoda předků jeAktivni() vrací true), vykonal destruktor stejnou funkci jako zavolání metody Radio::ukonciSpojeni().
class Prijimac Název atributu/metody()
Datový typ
specifikátor
aktivni
bool
private
prijimaci frekvence
float
private
jeAktivni()
bool
protected
prijimej(float )
void
public
prijimej()
void
public
ukonciPrijimani()
void
public
class Vysilac Název atributu/metody()
Datový typ
specifikátor
aktivni
bool
private
(pokračování tabulky na dalš
16
Objektově orientované programování
vysilaci frekvence
float
private
jeAktivni()
bool
protected
vysilej(float )
void
public
vysilej()
void
public
ukonciVysilani()
void
public
class Radio Název atributu/metody()
Datový typ
specifikátor
frekvence
float
private
ukonci spojeni()
void
public
ustav spojeni()
void
public
Grafické uživatelské rozhraní
Uvažujte abstrakci používanou v grafických uživatelských rozaních: okno. Pozor, celý problém nebude propracován tak podrobně, jak by implemntace v reálném sytému s GUI vyžadalovala. Naše třída Okno má atributy vyska, sirka,poziceX, poziceY typu int. V konstruktoru se skrze příchozí argumenty nastavuje vyska, sirka, poziceX a poziceY. Třídu okno dědí třída DialogovyBox. Ta má jako třídní atribut pole - kontejner tří objektů typu Zobrazitelne. Třída má metodu zobrazOkno(), která vypíše hlášku zobrazuji okno. Dále pak metodu zavriOkno(), která vypíše hlášku zaviram okno (dialogovy box). Třída Zobrazitelne je třída, poskytující metodu vykresli(). Tato metoda bude po zavolání vypisovat inaci o objektu, který vykresluje. Z třídy Zobrazitelne dědí třída Tlacitko, CheckBox a TextBox. Všechny třídy redefinují svoji vlastní metodu vykresli(). Třída Tlacitko bude vypisovat hlášku vykresleni tlacitka. Analogicky pak třídy CheckBox a TextBox. Tlacitko má atribut typu bool stisknuto, který se nastaví v konstruktoru na false. Tento atribut lze nastavit na true zavoláním metody stiskni() a jeho stav volani meotdy byloStisknuto(), která vrací typ bool. Třída TextBox má atribut text typu string, který je nastaven na prázdný řetězec, jako výchozí. Atribut text Může být změněn/nastaven metodou nastav(), přebírající argument typu string. Třída CheckBox má atribut stitek typu string, který se nastaví dle argumentu v konstruktoru (Všimněte si, jak se posílají argumenty zahnízděných objektů dané třídy!). Každý z těchto tří prvků zobrazuje při vytváření inaci v rámci volání konstruktoru. Pozor na pořadí zápisu tříd! Třída Zobrazitelne musí být umístěna před definicí třídy Dialogovy box. Další třída ModalniBox dědí ze třídy DialogovyBox. Třída redefinuje metodu zavriOkno, která vypíše hlášku ”zaviram okno (modalni box)” tehdy a jen tehdy, když atribut stisknuto objektu Tlacitko je true. V opačném případě vypíše hlášku ”okno nelze zavřít.”
17
Pokročilé techniky OON
class Okno Název atributu/metody()
Datový typ
specifikátor
Okno()
-
public
vyska
int
protected
sirka
int
protected
poziceX
int
protected
poziceY
int
protected
˜Okno()
-
public
class DialogovyBox Název atributu/metody()
Datový typ
specifikátor
DialogovyBox()
-
public
textbox
TextBox
protected
tlacitko
Tlacitko
protected
checkbox
Tlacitko
protected
zobrazOkno()
void
public
zavriOkno()
void
public
˜DialogovyBox()
-
public
class ModalniBox Název atributu/metody()
Datový typ
specifikátor
ModalniBox()
-
public
zavriOkno()
void
public
˜ModalniBox()
-
public
Název atributu/metody()
Datový typ
specifikátor
Zobrazitelne()
-
public
vykresli()
void
public
˜Zobrazitelne()
-
public
class Zobrazitelne
class Tlacitko Název atributu/metody()
Datový typ
specifikátor
Tlacitko()
-
public
vykresli()
void
public
˜Tlacitko()
-
public
class Tlacitko Název atributu/metody()
Datový typ
specifikátor
Tlacitko(string)
-
public
(pokračování tabulky na dalš
18
Objektově orientované programování
stitek
string
private
vykresli()
void
public
˜Tlacitko()
-
public
class TextBox Název atributu/metody()
Datový typ
specifikátor
TextBox()
-
public
text
string
private
nastav(string)
void
public
vykresli()
void
public
˜v()
-
public
class CheckBox Název atributu/metody()
Datový typ
specifikátor
CheckBox(string)
-
public
stitek
string
private
vykresli()
void
public
˜CheckBox()
-
public
1
Pokročilé techniky OON Test Způsob vyhodnocení: Při vyhodnocení budou za nesprávné odpovědi strhnuty body.
1. Vyberte seznam všech atributů, se kterými můžeme přímo manipulovat z hlavní funkce programu (a metod jiných tříd), pokud vytvoříme instanci třídy Auto? class Motor{ public: int vykon; private: int pocetValcu; public: Motor(int zadaneValce, int zadanyVykon){ pocetValcu = zadaneValce; vykon = zadanyVykon;
1
} int vratPocetValcu(){ return pocetValcu;
} }; class Auto{ protected: string barva; public: Motor* mujMotor; int rokVyroby; Auto(int vykon, int valce, string barvicka){ mujMotor = new Motor(valce, vykon); barva = barvicka;
} ~Auto(){ delete mujMotor;
} }; Vyberte jen jednu z následujících možných odpovědí.
a) b) c) d) e)
rokVyroby, rokVyroby, rokVyroby rokVyroby, rokVyroby,
barva, vykon, pocetValcu vykon barva barva, vykon
2. Mějme následující kód. Co se vypíše na obrazovku, pokud program spustíme? #include using namespace std; class Objekt{ public: void kdoJsem(){ cout << "Objekt";
} }; class Ctverec : public Objekt{ public: void kdoJsem(){ cout << "Ctverec";
} }; int main() {
1
2
Objektově orientované programování
Objekt* instance = new Ctverec; instance->kdoJsem(); delete instance; return 0;
} Vyberte jen jednu z následujících možných odpovědí.
a) Objekt b) Ctverec c) Instanci nepůjde popsaným způsobem vytvořit, program je chybný. 3. Mějme následující minimální kostru tříd (může být samozřejmě dále rozšířena). Ze kterých míst je možné zapisovat do atributu ”prvni”, pokud vytvoříme instanci třídy C? class A{ public: int prvni; }; class B{ public: A* odkaz;
1
B(){ odkaz = new A;
} ~B(){ delete odkaz;
} }; class C{ public: B* odkaz; C(){ odkaz = new B;
} ~C(){ delete odkaz;
} }; Vyberte libovolný počet možných odpovědí. Správná nemusí být žádná, ale také mohou být správné všechny.
a) b) c) d)
hlavní funkce programu a metod jiných tříd, než A, B a C metod třídy B metod třídy C metod třídy A
4. Mějme následující kód. Co se vypíše na obrazovku po jeho provedení? #include using namespace std; class Objekt{ public: virtual ~Objekt(){ cout << "Konec objektu ";
} }; class Ctverec : public Objekt{ public: ~Ctverec(){ cout << "Konec ctverce ";
2
Pokročilé techniky OON
} }; int main() { Objekt* instance = new Ctverec; delete instance; return 0;
} Vyberte jen jednu z následujících možných odpovědí.
a) b) c) d)
Konec ctverce Konec objektu Konec objektu Konec ctverce Konec ctverce Program je chybný. Třídy, které obsahují pouze destruktor a implicitní konstruktor nemohou existovat. e) Konec objektu
Návrhové vzory
Návrhové vzory • Návrhové vzory (Design patterns) můžeme s nadsázkou označit za “fintu”, jak řešit určitý problém nejen při návrhu OO systému.
• Existuje 23 základních vzorů, od kterých se odvozují další. • NV byly poprvé popsány v knize Design Patterns autorů Gama, Helm, Johnson, Vlissides (označováni jako ”Gang of Four”) Vzory dělíme podle jejich účelu do tří základních kategorií: 1. Tvořivé vzory: jak skrýt implementaci objektů, aby nebylo nutné při jejich změně měnit systém. 2. Strukturální změny: zabývají se propojením objektů. Jak propojení omezit nebo předejít změnám propojení. 3. Vzory chování: zapouzdřují určité procesy prováděné v systému (sort). Jedním nástrojem, který není návrhovým vzorem, ale má k němu blízko je ”sběrný parametr”. Jedná se o konteiner, který je předáván metodám a ty do něj umisťují data. Výhodou je, že metody si nemusí předávat data navzájem, ale používají neutrální ”přepravku”.
class SbernyParametr:public vector<string>{}; class Vypocty{ void metoda1(sbernyParametr* sp){ sp->push back(‘‘Prvni naklad");
} void metoda2(sbernyParametr* sp){ sp->push back(‘‘Dalsi naklad");
} }; int main(void){ Vypocty* poctarna = new Vypocty; SbernyParametr* sber = new SbernyParametr; poctarna->metoda1(sber); poctarna->metoda2(sber); ...
}
3
4
Objektově orientované programování
Několik základních vzorů Příkaz: výběr operace
• Příkaz (Command) je přepravka, ve které objektu předáte pokyn, co má udělat. • Vede k výraznému snížení provázanosti. • Princip: příkaz obalíme do objektu a předáme ho metodě.
class Prikaz{ public: virtual void proved()=0; }; class Privitani:public Prikaz{ void proved(){ cout << "Dobry vecer deti..." << endl;
} }; class Vecernicek{ private: Prikaz* prikaz; public: void ulozPrikaz(Prikaz* novyPrikaz){ prikaz = novyPrikaz;
} void provedPrikaz(){ prikaz->proved();
} }; int main(void){ Vecernicek* vecernik = new Vecernicek; Privitani* uvod = new Privitani; // rozloučení bude obdobné jako přivítání, jen vypisovat jiný text Rozlouceni* zaver = new Rozlouceni; vecernik->ulozPrikaz(uvod); vecernik->provedPrikaz(); vecernik->ulozPrikaz(zaver); vecernik->provedPrikaz(); ...
}
Zástupce: předvoj objektu Zástupce (Proxy) slouží jako prostředník pro přístup k různým implementacím určitého rozhraní.
Pokročilé techniky OON
class Rozhranni{ public: virtual void metoda1()=0; virtual void metoda2()=0; virtual void metoda3()=0; }; class Implementace : public Rozhranni{ public: void metoda1(){ cout << "Implementace metody1" << endl;
} void metoda2(){ cout << "Implementace metody2" << endl;
} ... }; class Zastupce : public Rozhranni{ private: Rozhranni* implementace; public: Zastupce(){ implementace = new Implementace; } ~Zastupce(){ delete(implementace); } // rozhranni void metoda1(){ implementace->metoda1(); } void metoda2(){ implementace->metoda2(); } void metoda3(){ implementace->metoda3(); } };
Adaptér
• Adaptér je obdobou Zástupce. • Účelem adaptéru je že vytvoří ”převodník” pro metody z jiným rozhraním. • Používá se např. při přetypovávání parametrů, při přístupu k již hotovým knihovnám, atp.
Stav: změny chování
• Pokud řešíme ve většině metod série obdobných podmínek, můžeme často na návrh aplikovat vzor stav (State).
• Stav slouží k rozdělení podmínek na stavy. Např.: – if(student == “nový”) {...} else {...} – stav “nový” a stav “starý”
• Tyto stavy jsou pak uloženy ve třídě jako její podtřídy. • Každá metoda podtřídy implementuje jen jednu část podmínky = přehledné. • Při přidání podmínek se jen vytvoří nové stavy = rozšiřitelné. Příklad bez použití návrhového vzoru
5
6
Objektově orientované programování
class Creature { bool isFrog; public: Creature() { isFrog = true; } void greet() { if(isFrog) cout << "Kvaaak!" << endl; else cout << "Nazdar!" << endl;
} void kiss() { isFrog = false; }
};
Po aplikaci návrhového vzoru:
class Creature { private: class State { public: virtual string response() = 0; }; class Frog : public State { public: string response() { return "Kvak!"; } }; class Prince : public State { public: string response() { return "Nazdar!"; } }; State* state; public: Creature() : state(new Frog()) {} void greet() { cout << state->response() << endl;
} void kiss() { delete state; state = new Prince();
} };
Je patrné, že kód se nám citelně prodloužil, nicméně jsme jej učinily bezpečnějším a snadněji rozšiřitelným. Základním problémem předchozího příkladu bylo, že při přidání stavu bylo nutné modifikovat již hotové metody. Tento krok je trochu ošemetný. Pokud je určitá metoda naprogramována, odladěna a otestována, měli bychom se snažit již do ní nezasahovat. To umožňuje tento návrhový vzor. S minimální modifikací existujícího kódu jsou přidávány nové stavy. Plný význam tohoto problému by byl samozřejmě patrný teprve na citelně delším příkladu, kdy stavy by byly komplexní kusy kódu. Strategie
• Jedná se o podobnou myšlenku jako u příkazu – kód rozdělíme na statický a dynamický. Dyn. pak podsouváme za běhu programu. (Skládání objektů)
• V tomto případě “kontext” řídí výběr použité “strategie”.
Pokročilé techniky OON
• Předpokládejme, že strategie je způsob řešení problému. Na základě kontextu se rozhodneme, jakou strategii zvolíme.
• Rozdíl oproti příkazu nespočívá ve struktuře, tak je téměř totožná, ale v myšlence aplikace: Příkaz je zabalení určité funkce do třídy a její předávání do jiných tříd pro vykonání. • Strategie je možností, jak dynamicky skládat různé třídy podle potřeby. Řetěz: zkoušení strategií
• Celý název je Řetěz zodpovědnosti (Chain of responsibility). • Podstata: Máme požadavek a snažíme se ho uspokojit. Existuje řada strategií (metod), které ho mohou uspokojit. Strategie zkoušíme za běhu postupně aplikovat, až nalezneme správnou nebo je všechny vyzkoušíme. • Obvykle se pro vyřízení požadavku volá pouze jedna funkce, v tomto případě se o to pokouší postupně mnoho funkcí (expertní systémy). • Řetěz můžeme také považovat za určitý dynamický “switch” (case v Pascalu).
Shrnutí Problematika návrhových vzorů přesahuje rámec kurzů Základy objektově orientovaného návrhu i Programování v jazyku C++. Toto téma jsem do přednášek zařadil z toho důvodu, abyste získali základní představu, co pojem návrhové vzory znamená a jak se v praxi využívá. Pro hlubší studium této problematiky doporučuji prameny uvedené v doporučené literatuře a Internet. Příklady
Příklad 1 Představme si situaci, že máme podnik, v něm jsou zaměstnáni programátoři, manažeři a případně další zaměstnanci. Všichni tito zaměstnanci mají většinu atributů společných, liší se jen v pár detailech. V praxi studenti často tento případ řeší (nevhodně) dědičností. My si ukážeme jaký má tento přístup nevýhody a jak (a hlavně proč) by bylo vhodné implementovat tento příklad pomocí jiných typů vazeb. Nejdříve se podívejme na špatnou variantu a zamysleme se nad tím, co se stane, když jeden zaměstnanec ve firmě změní svoji pracovní pozici (je povýšen). Zjistíme, že povýšení musíme realizovat vytvořením nové zaměstnance, přepsáním údajů a zrušením starého. Řešení:
#include using namespace std; class Zamestnanec{ public: string jmeno; string rodneCislo; }; class Programator: public Zamestnanec{ public: string jazyk; string skupina; }; class Manazer: public Zamestnanec{ public: string oddeleni; }; int main(){ Programator* pepa = new Programator; pepa->jmeno = "Karel"; pepa->rodneCislo = "794512/1234"; pepa->jazyk = "Prolog"; pepa->skupina = "grafici";
7
8
Objektově orientované programování
Manazer* pepa2 = new Manazer; pepa2->jmeno = pepa->jmeno; pepa2->rodneCislo = pepa->rodneCislo; pepa2->oddeleni = "Vyvojari"; delete(pepa); cout << "Jmeno: " << pepa2->jmeno << endl; delete(pepa2); return 0;
}
Příklad 2 Mějme stejný příklad, jako v předchozím příkladě. Nyní se podívejme, na vhodnější řešení. Je totiž nutné si uvědomit, že zaměstnání není typ člověka, ale je to role, kterou hraje v určité organizaci. Je tedy vhodné evidovat zvlášť informace o zaměstnanci a o jeho roli v podniku. Řešení:
#include using namespace std; class Zamestnani{ public: int plat; }; class Programator: public Zamestnani{ public: string jazyk; string skupina; }; class Manazer: public Zamestnani{ public: string oddeleni; }; class Zamestnanec{ public: string jmeno; string rodneCislo; Zamestnani* role; //Zamestnanec* asistent; }; int main(){ Zamestnanec* pepa = new Zamestnanec; Programator* grafik = new Programator; grafik->jazyk = "C++"; grafik->skupina = "grafici"; grafik->plat = 1000; pepa->jmeno = "Karel"; pepa->rodneCislo = "794512/1234"; pepa->role = grafik; cout << "Jmeno: " << pepa->jmeno << endl; cout << "Plat: " << pepa->role->plat << endl; // zmena zamestnani delete(grafik); Manazer* vedouci = new Manazer; vedouci->plat = 10000; vedouci->oddeleni = "Vyvoj"; pepa->role = vedouci; cout << "Jmeno: " << pepa->jmeno << endl; cout << "Plat: " << pepa->role->plat << endl; delete(pepa); return 0;
Pokročilé techniky OON
}
Příklad 3 Navrhněte a implementuje jednoduchou kalkulačku, která umožní uživateli zjišťovat obsah různých geometrických tvarů (kruh, čtverec, aj.). Typ objektu si uživatel bude volit až v průběhu běhu programu. Využijte při návrhu pozdní vazby pro zjednodušení volání metod. Řešení:
#include using namespace std; class grafickyObjekt{ private: virtual void spoctiObsah()=0; public: virtual void zadejUdaje()=0; virtual void vypisObsah()=0; }; class Ctverec : public grafickyObjekt{ private: int delkaStrany; int obsah; void spoctiObsah(){ obsah = delkaStrany*delkaStrany;
} public: void zadejUdaje(){ cout << "Zadej hranu: "; cin >> delkaStrany;
} void vypisObsah(){ spoctiObsah(); cout << "Obsah ctverce je: " << obsah << endl;
} }; class Kruh : public grafickyObjekt{ private: int polomer; int obsah; void spoctiObsah(){ obsah = polomer*polomer*3.14;
} public: void zadejUdaje(){ cout << "Zadej polomer: "; cin >> polomer;
} void vypisObsah(){ spoctiObsah(); cout << "Obsah kruhu je: " << obsah << endl;
} }; int main (int argc, char * const argv[]) { grafickyObjekt* objekt; cout << "Zadejte (c) pro ctverec, (k) pro kruh: "; string rozhodnuti; cin >> rozhodnuti; if (rozhodnuti == "k") { objekt = new Kruh; } else { objekt = new Ctverec;
9
10
Objektově orientované programování
} objekt->zadejUdaje(); objekt->vypisObsah(); delete(objekt); return 0;
}
Souhrn Shrnutí Po přečtení této kapitoly byste měli mít jasno v pojmech jako třída, instance, objekt, atribut, metoda, atp. Jejich znalost je nezbytná pro pochopení učiva v následující kapitole. Dále musíte mít přehled o popsaných typech vazeb a bezpečně rozlišovat, čím se liší, resp. kdy kterou použít. Jako doplnění stručných poznámek, kterými tento text je, doporučuji knihovnu a internet. Sám jsem čerpal zejména z: • Bruce Eckel - Myslíme v jazyku C++ (Myslíme v jazky Java) • Bruce Eckel - Thinking in C++ 14 (anglická verze knihy, která je dostupná na Internetu) • Miroslav Virius - Od C k C++ • Builder.cz - Základy OOP v C++ 15
Příklady Příklad 1 Vytvořte třídu Krida, která je modelem kousku křídy (Robotický učitel se snaží zjistit, kterou křídu má ze stolu zvednout, aby mohl psát na tabuli). Definujte alespoň osm atributů (včetně typů) a tři přístupové metody. Napište program, který definuje tuto třídu, vytvoří objekt, naplní jeho atributy a vypíše je na obrazovku. Zamyslete se nad užitečností jednotlivých atributů pro někoho, kdo chce křídou pouze psát na tabuli. Definujte ve třídě Krida jedinou metodu o kterou má zájem, ten kdo chce křídou psát. Řešení: Název atributu
Hodnota atributu
Datový typ
Šířka
2 cm
číslo
Výška
2 cm
číslo
Délka
8 cm
číslo
Hmotnost
xx g
číslo
Obsah vody
15%
číslo
Hustota
1400 kg/m
číslo
Tvrdost
xx
číslo
Barva
bílá
výčet
Zlomená
ano
bool
enum tBarva {BILA, MODRA, ZLUTA}; // deklrace tridy class Krida { private: // nasledujic polozky jsou pristupne pouze ze tridy Krida int sirka; int vyska; int delka; tBarva barva; 14 15
http://http://www.mindview.net/Books/TICPP/ThinkingInCPP2e.html http://www.builder.cz/art/cpp/cpp oop.html
Pokročilé techniky OON
public: //nasledujici polozky v teto tride jsou verejne //atributy float hustota; float obsahVody; int tvrdost; bool zlomena; //metody Krida(); //konstruktor ~Krida(); //destruktor int getSirka(); int getVyska(); int getDelka(); tBarva getBarva(); void setSirka(int value); void setVyska(int value); void setDelka(int value); void setBarva(tBarva value); float getHmotnost(); bool jdePsat(); }; //konstruktor tridy Krida::Krida() { cout << "Byl vytvoren objekt tridy Krida\n";
} //destruktor tridy Krida::~Krida() { cout << "\nByl zrusen objekt tridy Krida";
} //zjisteni hodnoty vysky int Krida::getVyska() { return vyska;
} //zjisteni hodnoty sirky int Krida::getSirka() { return sirka;
} //zjisteni hodnoty delky int Krida::getDelka() { return delka;
} // zjisteni hmotnosti cihly float Krida::getHmotnost() { return sirka*vyska*delka*1.1;
} //zjisteni hodnoty barvy tBarva Krida::getBarva() { return barva;
} //zjisteni zda je mozne kridou psat bool Krida::jdePsat() { return (sirka > 1) && (delka > 1) && (vyska > 1) && (obsahVody < 10);
} //nastaveni sirky void Krida::setSirka(int value) { if (value > 0) { sirka = value; } else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } //nastaveni vysky
11
12
Objektově orientované programování
void Krida::setVyska(int value) { if (value > 0) { vyska = value; } else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } //nastaveni delky void Krida::setDelka(int value) { if (value > 0) { delka = value; } else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } //nastaveni barvy void Krida::setBarva(tBarva value) { barva = value;
} Nejdůležitější údaj je možnost psaní křídou, tento údaj je nějakým způsobem závislý prakticky na všech ostatních atributech (kromě barvy). Model křídy má množství vlastností, z nichž však není snadné odvodit ty vlastnosti, které jsou podstatné z hlediska použiti modelu. Současně však nelze udržovat pouze metodu (nebo atribut) muzePsat, protoze závisí na ostatních atributech, které se neustále mění (např. délka).
Příklad 2 Popište včerejší Lidové noviny z hlediska spotřebitele. Vytvořte model objektu Lidovky, zapište jednotlivé atributy (včetně datových typů) do tabulky a definujte třídu Noviny v C++ a v UML. Řešení: Název atributu
Hodnota atributu
Datový typ
název
Lidové noviny
řetězec
datum
xx
datum
počet stran
12
číslo
formát
A4
výčet
// deklarace tridy class Kniha { public: //nasledujici polozky v teto tride jsou verejne //atributy string nazev; string autor; string vydavatel; string isbn; int rok; //metody Kniha(); //konstruktor ~Kniha(); //destruktor
};
13
Pokročilé techniky OON
Příklad 3 Popište knihu „Holistická detektivní kancelář Dirka Gentlyho“ z hlediska databáze knihovny. Vytvořte model Kniha včetně atributů a datových typů, zapište definici modelu v C++ a UML. Řešení: Název atributu
Hodnota atributu
Datový typ
název
Holistická detektivní kancelář Dirka Gentlyho Douglas Adams
řetězec
autor
řetězec
vydavatel
Argo
řetězec
ISBN
80-7203-044-2
řetězec
rok
1997
číslo
// deklarace tridy class Kniha { public: //nasledujici polozky v teto tride jsou verejne //atributy string nazev; string autor; string vydavatel; string isbn; int rok; //metody Kniha(); //konstruktor ~Kniha(); //destruktor
};
Příklad 4 Objekt (třída) je model reality. Při vytváření modelu musíme znát jeho aplikaci a soustředit se pouze na užitečné vlastnosti, univerzální model nelze vytvořit. • Teoretický rozbor: Popište slovy automobil vjíždějící do myčky z hlediska obsluhy myčky aut. Vozidlo je Škoda Octavia s nasazenou anténou, otevřenými okny a nezaplaceným programem. (motivace: vedoucí myčky chce zjistit, jestli není možné zrychlit provoz v myčce kontrolou připravenosti aut). Vytvořte UML model auta, pojmenujte jednotlivé atributy a zapište jejich datové typy do tabulky. Soustřeďte se pouze na atributy, které opravdu zajímají obsluhu myčky.
• Programové řešení: Napište program, který definujte třídu AutoMycka včetně konstruktoru a destruktoru. Vytvořte objekt této třídy, naplňte všechny atributy objektu a pak je vypište na obrazovku. Definujte také potřebné výčtové typy. Jak by vypadala definice třidy AutoMycka z pohledu řidiče? Řešení: Název atributu
Hodnota atributu
Datový typ
výška
1431
číslo
šířka
1731
číslo
anténa
ano
binární hodnota
otevřená okna
ano
binární hodnota
mycí program
žádný
výče
14
Objektově orientované programování Je zřejmé, že automobil ma množství jiných vlastností, z hlediska myčky jsou však nevýznamné a není tedy nutné ani vhodné je definovat. #include using namespace std; //povinna hlavicka // pomocne vyctove typy enum tProgram {ZADNY, SPRCHOVANI, VOSKOVANI, ZDIMANI}; // deklrace tridy class AutoMycka { public: //nasledujici polozky v teto tride jsou verejne //atributy int vyska; //pokud uvadime rozmery v milimetrech muzeme pouzit typ int int sirka; bool antena; bool otevrenaOkna; tProgram myciProgram; //metody AutoMycka(); //konstruktor ~AutoMycka(); //destruktor }; //konstruktor tridy AutoMycka::AutoMycka() { cout << "Byl vytvoren objekt tridy AutoMycka\n";
} //destruktor tridy AutoMycka::~AutoMycka() { cout << "\nByl zrusen objekt tridy AutoMycka";
} int main(int argc, char *argv[]) { AutoMycka *skodaOctavia; // promenna skodaOctavia = new AutoMycka; // vytvoreni objektu //naplneni atributu objektu skodaOctavia->vyska = 1431; skodaOctavia->sirka = 1731; skodaOctavia->antena = true; skodaOctavia->otevrenaOkna = true; skodaOctavia->myciProgram = ZADNY; //vypsani vlastnosti objektu cout << "\nVyska: " << skodaOctavia->vyska << " mm"; cout << "\nSirka: " << skodaOctavia->sirka << " mm"; cout << "\nAntena: " << skodaOctavia->antena; cout << "\nOtevrena okna: " << skodaOctavia->otevrenaOkna; cout << "\nMyci program: " << skodaOctavia->myciProgram; delete skodaOctavia; //zruseni objektu return 0;
}
Příklad 5 Popište bílý list papíru A4 na kterém je černým fixem napsán text „Něco duchaplného“. Definujte třídu tohoto objektu, pojmenujte jednotlivé atributy a zapište jejich datové typy. Definujte alespoň 10 atributů. Napište program, který definujte třídu ListPapiru. Definujte také potřebné výčtové typy. Definujte přístupové metody k atributům formát, výška, šířka. Metody zajistí správné nastavení formátu papíru při změně šířky nebo výšky a naopak správné nastavení výšky a šířky při změně formátu. Současně vhodně definujte modifikátory viditelnosti k zamezení/povolení přístupu k atributům a metodám třídy. Řešení: název atributu
hodnota atributu
datový typ
(pokračování tabulky na další straně)
15
Pokročilé techniky OON
výška
6 cm
číslo
šířka
8 cm
číslo
délka
20 cm
číslo
hmotnost
xx g
číslo
hustota
2100 kg/m
číslo
/* Zdrojovy kod prikladu: varianta 1 */ enum tFormat {A0, A1, A2, A3, A4, VLASTNI}; enum tBarva {BILA, CERNA, MODRA, CERVENA, ZLUTA}; enum tPisatko {FIX, TUZKA}; enum tOrientace {SVISLE, VODOROVNE}; // deklarace tridy class ListPapiru { private: //atributy tFormat format; int sirka; int vyska; //metody void nastavFormat(); public: //nasledujici polozky v teto tride jsou verejne //atributy string text; tBarva barvaPapiru; tBarva barvaTextu; tPisatko pisatko; float tloustka; int hmotnost; tOrientace orientace; //metody ListPapiru(); //konstruktor ~ListPapiru(); //destruktor void setFormat(tFormat value); void setVyska(int value); void setSirka(int value); tFormat getFormat(); int getVyska(); int getSirka(); }; // nastavi format papiru podle aktualne nastavene vysky a a sirky void ListPapiru::nastavFormat() { if ((sirka == 210) && (vyska == 297)) { format = A4; } else if ((sirka == 297) && (vyska == 420)) { format = A3; } else { format = VLASTNI;
} } //konstruktor tridy ListPapiru::ListPapiru() { cout << "Byl vytvoren objekt tridy ListPapiru\n";
} //destruktor tridy ListPapiru::~ListPapiru() { cout << "\nByl zrusen objekt tridy ListPapiru";
} // nastaveni formatu papiru void ListPapiru::setFormat(tFormat value) {
16
Objektově orientované programování
format = value; switch (value) { case A4: sirka = 210; vyska = 297; break; case A3: sirka = 297; vyska = 420; break;
} } // nastaveni sirky papiru void ListPapiru::setSirka(int value) { sirka = value; nastavFormat();
} // nastaveni vysky papiru void ListPapiru::setVyska(int value) { vyska = value; nastavFormat();
} // zjisteni hodnoty format papiru tFormat ListPapiru::getFormat() { return format;
} // zjisteni sirky papiru int ListPapiru::getSirka() { return sirka;
} // zjisteni vysky papiru int ListPapiru::getVyska() { return vyska; }/* Zdrojovy kod prikladu: varianta 2 */ class ListPapiru { private: //atributy int sirka; int vyska; public: //nasledujici polozky v teto tride jsou verejne //atributy string text; tBarva barvaPapiru; tBarva barvaTextu; tPisatko pisatko; float tloustka; int hmotnost; tOrientace orientace; //metody ListPapiru(); //konstruktor ~ListPapiru(); //destruktor void setFormat(tFormat value); void setVyska(int value); void setSirka(int value); tFormat getFormat(); int getVyska(); int getSirka(); }; //konstruktor tridy ListPapiru::ListPapiru() { cout << "Byl vytvoren objekt tridy ListPapiru\n";
} //destruktor tridy ListPapiru::~ListPapiru() { cout << "\nByl zrusen objekt tridy ListPapiru";
} void ListPapiru::setFormat(tFormat value) { switch (value) {
17
Pokročilé techniky OON
case A4: sirka = 210; vyska = 297; break; case A3: sirka = 297; vyska = 420; break;
} } void ListPapiru::setSirka(int value) { sirka = value;
} void ListPapiru::setVyska(int value) { vyska = value;
} tFormat ListPapiru::getFormat() { if ((sirka == 210) && (vyska == 297)) { return A4; } else if ((sirka == 297) && (vyska == 420)) { return A3; } else { return VLASTNI;
} /* Po zruseni atributu format je potreba hodnotu format zjistit pri kazdem dotazu, takto definovany objekt je jednodussi, pokud zjisteni hodnoty neni narocna operace. */
} int ListPapiru::getSirka() { return sirka;
} int ListPapiru::getVyska() { return vyska;
} Ve druhé variantě není atribut Formát definovaný, respektive není definovaná proměnná, ve které by byla jeho hodnota uložena. Hodnota se tedy vždy zjistí z výšky a šířky při každém dotazu na tuto hodnotu. Vlastní implementace je jednodušší, motivací pro použití první varianty může být náročnost zjištění hodnoty atributu. Pokud zjištění hodnoty atributu reprezentuje složitý (několikaminutový) výpočet nebo například přenos dat po síti, je zpravidla druhý způsob nepoužitelný.
Příklad 6 Definujte třídu Cihla popisujicí objekt obyčejné plné cihly. Definujte alespoň dva vzájemně závislé a užitečné atributy a jejich přístupové metody, atributy třídy zapište do tabulky. Napište program v C++, který implementuje třídu Cihla a vytvořte diagram UML. Řešení: název atributu
hodnota atributu
datový typ
výška
6 cm
číslo
šířka
8 cm
číslo
délka
20 cm
číslo
hmotnost
xx g
číslo
hustota
2100 kg/m
číslo
// deklarace tridy class Cihla { private: // nasledujici polozky jsou pristupne pouze z tridy Cihla int vyska; int sirka; int delka; float hustota;
18
Objektově orientované programování
public: //nasledujici polozky v teto tride jsou verejne //metody Cihla(); //konstruktor ~Cihla(); //destruktor int getVyska(); int getSirka(); int getDelka(); float getHustota(); void setVyska(int value); void setSirka(int value); void setDelka(int value); void setHustota(float value); float getHmotnost(); /* hmotnost nelze nastavit protoze neurcuje jednoznacne ostatni vlastnosti */ }; //konstruktor tridy Cihla::Cihla() { cout << "Byl vytvoren objekt tridy Cihla\n";
} //destruktor tridy Cihla::~Cihla() { cout << "\nByl zrusen objekt tridy Cihla";
} // zjisteni hodnoty vysky int Cihla::getVyska() { return vyska;
} // zjisteni hodnoty sirky int Cihla::getSirka() { return sirka;
} // zjisteni hodnoty delky int Cihla::getDelka() { return delka;
} // zjisteni hodnoty husoty float Cihla::getHustota() { return hustota;
} // zjisteni hmotnosti cihly float Cihla::getHmotnost() { return sirka*vyska*delka*hustota;
} // nastaveni sirky void Cihla::setSirka(int value) { if (value > 0) { sirka = value; } else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } // nastaveni vysky void Cihla::setVyska(int value) { if (value > 0) { vyska = value; } else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } // nastaveni delky void Cihla::setDelka(int value) { if (value > 0) {
19
Pokročilé techniky OON
delka = value;
} else { cout << "\nPokus o nastaveni nepripustneho rozmeru: " << value;
} } // nastaveni hustoty void Cihla::setHustota(float value) { if (value > 0) { hustota = value; } else { cout << "\nPokus o nastaveni nepripustne hustoty: " << value;
} } Vlastnost hmotnost je plně určena rozměry a hustotou, lze ji tedy číst, nelze ji však zapisovat, protože ze zadané hmotnosti nelze stanovit rozměry a hustotu. Metody pro nastavení rozměrů a hustoty obsahují v implementaci jednoduchou kontrolu korektních hodnot. Tím se zpravidla zjednoduší implementace dalších funkcí ve třídě, kde již lze předpokládat, že hodnoty jsou vždy v povoleném rozsahu. Nastavovat hmotnost by teoreticky bylo možné ve vykonstruovaném případě, kdy by se objekt Cihla využíval pro zodpovězení dotazu ”Jaké musí být rozměry cihly, aby hmotnost byla x?”. Toto řešení je však nevhodné, protože metoda setHmotnost by měla neprůhledné vedlejší efekty (změnu velikosti), spravné řešení by tedy bylo definovat specialní metodu nastavVhodneRozmery(danaHmotnost).
Příklad 7 Definujte třídu Sklenice, která popisuje válcovou sklenici bez potisku z hlediska uživatele. Definujte konstruktor a desturktor třídy, definujte vzájemě závislé (a přesto užitečné atributy) a metody pro jejich čtení a zápis. Zapište jednotlivé atributy do tabulky. Řešení: název atributu
hodnota atributu
datový typ
výška
10 cm
číslo
průměr
8 cm
číslo
objem
500 cm
číslo
materiál
čiré sklo
výčet
class Sklenice { private: int vyska; int prumer; public: //nasledujici polozky v teto tride jsou verejne //atributy tMaterial material; //metody Sklenice(); //konstruktor ~Sklenice(); //destruktor int getVyska(); int getPrumer(); void setVyska(int value); void setPrumer(int value); float getObjem();
}; //konstruktor tridy
20
Objektově orientované programování
Sklenice::Sklenice() { cout << "Byl vytvoren objekt tridy Sklenice\n";
} // destruktor tridy Sklenice::~Sklenice() { cout << "\nByl zrusen objekt tridy Sklenice";
} // zjisteni hodnoty vysky int Sklenice::getVyska() { return vyska;
} // zjisteni hodnoty prumeru int Sklenice::getPrumer() { return prumer;
} // nastaveni prumeru void Sklenice::setPrumer(int value) { prumer = value;
} // nastaveni vysky void Sklenice::setVyska(int value) { vyska = value;
} // zjisteni obejmu sklenice float Sklenice::getObjem() { return (3.145*prumer*prumer*vyska) / 4;
} V tomto případě je sice objem jednoznačně určen výškou a průměrem sklenice, rozměry sklenice však nejsou jednoznačně určené objemem. Nastavení objemu tedy ztrácí smysl a objem v tomto případě definujeme jako atribut, který je pouze pro čtení.
Příklad 8 Prvňáček používá sešit a kalkulačku. Nadefinujte třídy Prvnacek, obsahující metody napisDoSesitu a vypocitej, Sesit obsahující metodu pripis a Kalkulacka obsahující metody plus a minus. Implementujte hlavní program demonstrující zasílání zpráv mezi prvňáčkem, sešitem a kalkulačkou.
Příklad 9 Vymodelujte vztahy mezi zaměstnancem, zaměstnavatelem a jejich účty v době výplaty. Nakreslete UML a implementujte v C++. Zamyslete se nad tím, jak uvedenou situaci vymodelovat s využitím agregace i bez ní. Řešení: Třída Ucet
Název atributu
Hodnota atributu
Datový typ
stav
1 000 000
číslo
Metody: vklad, vyber, odesliPenize Třída Clovek
21
Pokročilé techniky OON
Název atributu
Hodnota atributu
Datový typ
Jméno
Franz
řetězec
Příjmení
Mayer
řetězec
Funkce
ředitel
řetězec
Účet
Metody: posliPenize, getUcet, kolikMasNaUctu
Ucet
22
Práce s pokročilými vývojovými nástroji
3
Práce s pokročilými vývojovými nástroji
Co v této kapitole naleznete
Co v této kapitole naleznete Do této kapitoly budou postupně přibývat ukázky práce s pokročilými vývojovými nástroji jako jsou Visual Paradigm 16 , Microsoft Visual Studio 17 , Dev C++ 18 a jiné. Jednotlivé části kapitoly budou vždy řešit určitý problém v daném vývojové nástroji. Prvním krokem je uveřejnění několika krátkých návodů na práci s programem Visual Paradigm 19 . Tento software je licencován na Provozně ekonomické faktultě MZLU v Brně pro všechny studenty kurzů objektového programování. Mohou si jej bezplatně stáhnout ve verzi Standard 20 a užívat na libovolném množství počítačů. K využívání je nutné používat licenční klíče 21 pro studenty. Také poměrně kvalitní vývojové prostředí Microsoft Visual Studio 22 je k dispozici všem studentům PEF zdarma v rámci MSDAA 23 licence, resp. Campus licence.
Animace: Visual Paradigm – vytvoření diagramu tříd a základní třídy
Obsahem této části je následující odkaz: https://akela.mendelu.cz/˜xproch17/opory/cpp/Visual Paradigm vytvoreni tridy/Visual Paradig
Animace: Visual Paradigm – tvorba a nastavení základním vazeb v diagramu tříd
Obsahem této části je následující odkaz: https://akela.mendelu.cz/˜xproch17/opory/cpp/Visual Paradigm vazby/Visual Paradigm vazby.
Animace: Visual Paradigm – generování kódu z diagramu tříd a rekonstrukce diagramu tříd z kódu
Obsahem této části je následující odkaz: https://akela.mendelu.cz/˜xproch17/opory/cpp/Visual Paradigm generovani kodu/Visual Parad
16
http://www.visual-paradigm.com/ http://www.microsoft.com/cze/msdn/produkty/vstudio/default.mspx 18 http://www.bloodshed.net/devcpp.html 19 http://www.visual-paradigm.com/ 20 http://indica.mendelu.cz/vpapp/ 21 http://akela.mendelu.cz/˜xproch17/keys.zip 22 http://www.microsoft.com/cze/msdn/produkty/vstudio/default.mspx 23 http://www.pef.mendelu.cz/msdna/ 17
Práce s řetězci, soubory a výjimky
4
Práce s řetězci, soubory a výjimky
Soubory pomocí proudů
Soubory a C++ Koncepce práce se soubory je v C++ oproti C značně odlišná, stejně jako u práce s terminálem. Koncepce načítání a ukládání dat ze/do souborů je stejná jako u terminálu. Příkazy jsou prakticky zcela stejné. Rozdíl je pouze ve vytvořeném proudu. Podívejme se nejprve na textové soubory.
Textové soubory Při práci s textovými soubory používáme zpravidla proudy ifstream (input file stream) pro načítání dat a ofstream (output file stream) pro zápis. Vytvářet tyto proudy nám pomáhá knihovna fstream. Prvním krokem k otevření souboru je deklarace výstupního proudu. Druhým krokem je jeho otevření.
#include #include using namespace std; int main(){ ofstream out; out.open("temp.txt"); for(int i=1;i<=10;i++) { out << i << endl;
} out.close(); // Nezapominejme soubor po ukonceni prace zavrit!
}
Otevření souboru můžeme zkrátit, pokud otevření souboru spojíme s deklarací proudu – oftream out("temp.txt"). Ještě je vhodné poznamenat, že soubory je nutné zavírat. Pokud soubor nezavřete, jednak k němu nemohou přistupovat jiné aplikace, ale hlavně hrozí to, že se do něj zapisovaná data fyzicky neuloží. Tento příklad však nebyl úplně ideální. Při zápisu jsem se spoléhal na to, že soubor se podařilo otevřít. To samozřejmě není úplně čisté. V praxi je vhodné kontrolovat, zda se otevření skutečně podařilo.
ofstream out; out.open("pokus.txt"); if (out.is open()) { out << "radka textu"; out.close(); } else cout << "Soubor se nepodarilo nacist...";
Čtení ze souboru probíhá obdobným způsobem, jako zápis. Nejtradičnější je využívání operátoru ”>>”, který umožňuje načítat vstupní soubor po slovech, resp. číslech (tedy po celcích znaků oddělených znaky jako jsou mezery, tečky atp.). Velkou výhodou tohoto přístupu je, že pokud máte v souboru skupinu čísel, můžete načítat přímo jednotlivá čísla u kládat je např. do proměnné typu int a nemusíte složitě načítat po číslicích a následně provádět konverze.
23
24
Práce s řetězci, soubory a výjimky
string slovo; ifstream in("temp.txt"); if (in.is open()) { while(in >> slovo) cout << slovo << " "; cout << endl;
} ...
Existuje samozřejmě i několik dalších způsobů, jak načítat data ze souborů:
char znak; char text[50]; ifstream in; in.open("pokus.txt"); in.get(znak); //precte znak a ulozi ho do prom. typu char in >> text; //precte slovo, uklada do stringu in.getline(text,50); //precte radek, ulozi do pole znaku in.read(text, 50); //precte zvoleny pocet znaku, ulozi do pole znaku in.close();
Často je potřeba uložit načtené pole znaků do proměnné typu string, to uděláme prostě přiřazením – string retezec = pole znaku. Pokud potřebujete provést opačnou konverzi (string na pole znaků), zavoláte metodu c str(), která ho vrátí – pole znaku = retezec.c str().
Nezapomeňte: pokud otevíráte soubor, musíte dát jako jméno souboru řetězec znaků nebo konkrétní název v uvozovkách. Pokud máte název souboru uložen ve stringu, musíte zavolat metodu c str() – open(nazevSouboru.c str()).
Režimy práce se souborem Soubory mohou být kromě normálního čtení nebo zápisu otevírány i v jiných módech. Mód se uvádí jako druhý parametr metody open – out.open("vystup.dat", ios base::binary). Zde je několik z nich:
• • • • • •
in – otevře soubor pro čtení (výchozí pro čtení, není nutné psát u ifstreamu). out – otevře soubor pro zápis (výchozí pro zápis, není nutné psát u ofstreamu). ate – po otevření hledej konec souboru. app – přidej text na konec souboru (velmi často využíváno). trunc – zkrat‘ existující soubor na nulovou délku. binary – binární režim (viz níže).
Práce s řetězci, soubory a výjimky
Zkuste si vytvořit jednoduchý kód pro kopii souboru po znacích (vylepšete příklad o kontrolu otevření souborů).
int main() { ifstream from("soubor1.txt"); ofstream to("soubor2.txt"); char ch; while(from.get(ch)) to.put(ch); from.close(); to.close();
}
Všimněte si, že cyklus while lze velmi efektně využívat pro načtení celého souboru, stačí dát příkaz čtení do podmínky cyklu a pokud se čtení zadaří, cyklus se provede.
Binární soubory Hlavním smyslem binárních souborů je usnadnit ukládání složitých struktur do souborů, resp. jejich načítání ze souborů. Představme si program, který pracuje s objekty třídy Zakazník, každý zákazník má řadu vlastností (jména, rodná čísla, aj.). Uložit tyto zákazníky do textového souboru by znamenalo postupné procházení všech objektů a jejich atributů a ukládání dat. Navíc by nastal problém s oddělením jednotlivých položek (kde končí jméno zákazníka a začíná jiné položka?). Museli by se vymýšlet různé oddělovače a jiné ”berličky”, které by umožnili zapsat tyto struktury včetně případné hierarchie. Abychom nemuseli tyto problémy řešit, využíváme často binární soubory. V případě práce s binárním souborem prostě řekneme ”ulož tento objekt do souboru” a je to. Soubor samozřejmě není normálně čitelný, obsahuje výpis paměti, ale o nic se nemusíme starat. Nevýhodou tohoto systému je omezená přenositelnost mezi platformami. Pokud vytvoříme bin. subor na jedné platformě a otevřeme na druhé, kde např. integer ma jiný počet bytů, mohou nastat problémy. Jsou však poměrně vzácné. Reálným záporem je spíše nečitelnost bin. souborů a z toho plynoucí uzavřenost (kdo nezná jejich strukturu, není je schopen načítat). Pro zápis do bin. souboru používáme funkci write, která má dva parametry – co se má zapsat a jak je to velké. Důležité je, že write ukládá jen pole znaků. Není však problém přetypovat jakoukouliv proměnnou na tento typ, beze ztráty informace. Následující příklad ilustruje problém na uložení pole čísel.
int pole[4]={1,2,3,4}; ofstream out; out.open("vystup.dat", ios base::binary); if (out.is open()) { out.write((char *)pole, sizeof(pole)); // pretypovani out.close(); } else cout << "Nepodarilo se otevrit soubor!" << endl;
25
26
Práce s řetězci, soubory a výjimky Čtení z binárního souboru probíhá totožným způsobem, jen místo funkce write voláme read.
char pole[4]; ifstream in; in.open("vstup.dat",ios base::binary); if (out.is open()) { in.read((char *)pole, sizeof(pole)); //pretypovani in.close(); } else cout << "Nepodarilo se otevrit soubor! \n";
Skoky v souboru Zvláště v souvislosti s binárními soubory využijeme možnosti posunovat se v souboru na určitou pozici. Např.: načíst až třetího zákazníka (bin. soubor) nebo vypsat posledních 10 znaků textového souboru. C++ umožňuje následující skoky v souborech:
• • • •
seekg(position) – skok na pozici ve výstupním proudu. seekp(position) – skok na pozici ve vstupním proudu. seekg(skok, pozice) – skok o zadaný počet bytů (vst.). seekp(skok, pozice) – skok o zadaný počet bytů (vyst.).
Mimo absolutních hodnot zadaných číselně rozlišujeme následující identifikátory pozice:
• beg – začátek souboru. • end – konec souboru. • cur – aktuální pozice v souboru.
Příklad: in.seekg(20, ios base::beg); //skok o 20 bytu od zacatku souboru
XML soubory Třetím typem souborů často využívaných v různých programech jsou XML soubory. XML v sobě do značné míry kombinují výhody textových a binárních souborů. Jsou čitelné pouhým okem a tím pádem je jejich struktura otevřená (do značné míry) a přitom umožňují zaznamenávat hierarchii a složité datové strktury. Jejich nevýhodou je, že přece jen jsou objemnější, než binární soubory a hlavně uložení dat do XML není tak prosté jako u bin. souborů. U rozsáhlých projeků, kde soubory mají sloužit jako platforma pro ukládání nebo dokonce přenos dat se však vyplatí o XML silně uvažovat. Práce s XML přesahuje problematiku základních kurzů programování a proto zde nebude podrobně probírána. Odkáži proto laskavého čtenáře na stránky různých knihoven pro zpracování XML. Pro C/C++ existují knihovny, které umožňují automatické zpracování XML: libxml2, Xerxes, expat, Arabica, aj. Jejich výčet naleznete na http://www.ibm.com/ developerworks/xml/library/x-ctlbx.html. Subjektivně však považuji práci s XML v C++ za méně komfortní, než třeba v jazycích Java nebo Python. Je to dáno zvláště obtížnější prací s řetězci a občas koncepcí knihoven, která by spíše odpovídala C, než C++.
Práce s řetězci, soubory a výjimky
Příklad XML souboru ukládájícího informace o různých poznámkách. Všimněte si, že každá poznámka obsahuje určité elemnety a všechny poznámky patří do elementu notes – poznámky. Je zde jasná hierarchie a přitom jsou informace jasně čitelné.
<notes> <note> Tove Jani Reminder Dont forget me this weekend! <note> ... ...
Práce s řetězci – třída String
Řetězce Práce s řetězci patřila v jazyku C k poměrně obtížným činnostem. Řetězce byly vnímány jako pole znaků. Z toho plynula řada nevýhod a hlavně nutnost nízkoúrovňového přístupu. C++ obsahuje třídu string, která tyto nevýhody eliminuje. Pokud chceme v C++ uložit řetězec, nadeklarujeme proměnnou typu string a řetězec jí přiřadíme. Nemusíme se starat o jeho délku ani nic jiného.
string veta; veta = "Ahoj Karle"; string jinaVeta = "Ahoj Karle"; string jesteJinaVeta("Ahoj Pepiku");
Na první pohled by se mohlo zdát, že se jedná o prostý datový typ (jako např. integer). Pokud se ale podíváme na poslední řádek předchozího příkladu, pozornému čtenáři neunikne, že se vlastně jedná o vytváření statických instancí, v daném případě dokonce volání parametrického konstruktoru. Ono přiřazení pomocí = samozřejmě není prostým přiřazením, jak ho známe např. u typu integer. V praxi se provádí citelně složitější operace, při které je obsah závorek uložen do určitého atributu instance třídy string.
27
28
Práce s řetězci, soubory a výjimky
Jiný příklad vytvoření stringu:
#include <string> using namespace std; int main() { string imBlank; string heyMom("Where are my socks?"); string standardReply = "Beamed into deep " "space on wide angle dispersion?"; string useThisOneAgain(standardReply);
}
Protože string je třída, manipulujeme s obsahem instancí této třídy pomocí řady různých metod. Tento výčet uvádí některé základní operace, které lze se stringem provádět: • kopie – s2 = s1, • porovnání – s2>s1 (<, ==, !=), • zápis na určité místo v řetězci – retezec[10] = a. • kopie části řetězce – s2 = s1.substr(10, 20) – 20 znaků od 10. pozice, • spojování řetězců – s3 = s1+" a "+s2, • připojení řetězce – s2.append(" konec"), • vkládání řetězce – s2.insert(2, " vlozeny text pred pozici 2 "), • smazání n znaků od pozice x – s.erase(x, n), • nahrazení podřetězce pomocí knihovny algorithm – replace(s.begin(), s.end(), co, cim), • nalezení podřetězce – pozice = retezec. nd(tag) – vrátí pozici prvního výskytu tagu, existuje i varianta pozice = retezec. nd(start, tag), kde hledání probíhá od pozice start, Pokud není podřetězec nalezen, vrátí se hodnota npos – if(s. nd(...) != string::npos)... • řada dalších vyhledávání: nd rst of(), nd rst not of(), nd last of(), ... • zjištění velikosti – retezec.size(), retezec.lenght(), • kolik můžeme zapsat, než se vyčerpá volné místo a bude se automaticky hledat nové – retezec.capacity(), • ruční rezervace místa pro řetězec – retezec.reserve(500).
K posledním třem příkazům je vhodé poznamenat, že paměť pro řetězec se alokuje automaticky. V okamžiku vytvoření instance je předem zaalokován určitý paměťový prostor pro znaky. Pokud se tento prostor vyčerpá, zaalokuje se automaticky nový, tato operace však stojí čas. Proto, pokud dopředu vím, že budu řetězec zvětšovat až do určité větší velikosti, je vhodné zaalokovat hned na začátku více místa. Tato operace samozřejmě není nijak zásadní, takže ji zmiňuji jen na okraj.
Dost často potřebujeme převést string na jiný datový typ. V nejčastěji na řetězec znaků. K tomu slouží metoda c str(), o které jsme už mluvili. Pokud potřebujete převést string na číslo doporučuji některou z variant příkazu ato..., např.: atoi().
Indexy a vstup na neplatnou pozici Do teď jsme používali pro identi kaci pozice v řetězci [] – retezec[3]. Kromě této metody můžeme využívat metodu at() – s.at(10).
Práce s řetězci, soubory a výjimky Výhodou je, že při vstupu na neplatnou pocizi funkce at() vyvolá výjimku, která je zachytitelná (ukážeme si v další kapitole), což se o neplatném přístupu do paměti říci nedá.
#include <exception> #include #include <string> using namespace std; int main() { string s("1234"); try { // specialni oblast, ve ktere se pokusim provest operaci, ktera muze vyvolat vyjimku s.at(5); } catch(exception& e) { // zavola se, pokud vyjimka nastane cerr << "Vstup na neplatnou pozici v retezci" << endl;
} ...
Výjimky
Výjimky Výjimky jsou mechanismus, kterým se snažíme zabránit pádu programu v důsledku provedení určité neplatné operace (zadání špatné hodnoty, přerušení TCP spojení, atp.). Takovéto situce se samozřejmě zpravidla snažíme řešit ”proaktivní ochrannou” - např. podmínkami. V některých okamžicích ale tento způsob ochrany zabere příliš mnoho práce nebo je dokonce (téměř) neproveditelný. V tomto okamžiku se nasazují výjimky.
Nevýhodou výjimek je, že spotřebovávají značné množství systémových prostředků. Proto je nutné užívat jich obezřetně. Rozhodně nenahrazují ošetřování kódu pomocí podmínek!
Výjimkou může být v principu i jednoduchý datový typ, ale zpravidla se jedná o kompletní třídu s atributy a metodami.
Výjimky jsou buď předdefinováné (např. metoda at() v třídě String) nebo je vytváříme ručně. Základní struktura výjimky je následující: Nebezpečnou operaci, která může zaznamenat chybu a v důsledku toho vyhodit výjimku zavoláme v bloku označeném try. Pokud v tomto bloku nastane vyhození výjimky, přeruší se okamžitě provádění tohoto bloku a hledá se nejbližší následující blok catch, který se postará o její ošetření. Ukažme si tento problém na příkladu metody at().
29
30
Práce s řetězci, soubory a výjimky
Nadeklarujeme si textový řetězec o 4 znacích. Pomocí metody at() se pokusíme přistoupit na 5. pozici. To způsobí chybu a vyvolá výjimku.
#include <exception> #include #include <string> using namespace std; int main() { string s("1234"); try {
// zde se můžeme pokusit provést operaci, která povede k výjimce
cout << "Pate pismeno je: " << s.at(5); // tato operace způsobí vyvolání výjimky cout << "ahoj"; // kód, který je tady se už nyní neprovede
} catch(exception& e) { // zde je výjimka odchycena cerr << e.what() << endl; } }
Je zřejmé, že předchozí příklad by šlo elegantně řešit pomocí podmínky. Před přístupem na 5. pozici prostě provedu kontrolu pomocí s.size(). Smyslem však bylo demonstrovat syntaxi podmínek. V hlavičce bloku catch máme napsáno exception& e. Znamená to, že odchytáváme výjimky třídy exception. Pokud u výjimek této třídy zavoláme metodu what(). Vrátí se důvod výjimky (např. přístup na neexistující pozici).
Ruční vytváření výjimek Pokud má mít některá funkce (metoda) možnost vyvolat výjimku, musí to být uvedeno v její deklaraci. Za závorku s parametry metody napíšeme klíčové slovo throw a následně jakou výjimku metoda vyvolává. Pokud může funkce vracet více typů výjimek, musí být v závorce všechny.
class Zlomek{ private: int citatel, jmenovatel; public: void nastavCitatel(int c) { citatel = c;
} void nastavJmenovatel(int j) { jmenovatel = j;
} double vydel() throw (Vyjimka); };
Nyní si musíme nadeklarovat třídu Vyjimka. Udělejme ji velmi jednoduchou:
Práce s řetězci, soubory a výjimky
class Vyjimka { private: string Duvod; public: void nastav(string d) { Duvod = d;
} string vratDuvod() { return Duvod;
} };
Posledním krokem bude podívat na implementaci metody vydel našeho zlomku, který bude touto výjimkou disponovat.
double Zlomek::vydel() throw (Vyjimka){ if (jmenovatel == 0) { Vyjimka v; v.nastav("Nejde delit, delitel je roven nule"); throw v;
} return ((double) citatel/jmenovatel);
}
Nyní se zamysleme, jaký smysl mělo přesunout kontrolu jmenovatele z programu do metody vydel. Rozdíl byl v tom, že jsem se rozhodl nepředpokládat tiše, že uživatel mojí třídy bude inteligentní a sám si jmenovatel kotroluje, ale raději jsem jeho kontrolu provedl explicitně při samotném dělení. Pokud byl uživatel chytrý a do jmenovatele 0 nedosadil, vše bude v pořádku. Pokud však chyba přeci jen nastane (uživatel neví, že se nesmí dělit nulou), program nespadne, případně neprovede jinou hloupost (zápis na neplatné místo v paměti, atp.), ale vrátí výjimku popisující podstatu chyby. Programátor tak bude schopen chybu velmi rychle najít a odladit. A takto vypadá program využívající naše třídy:
Zlomek z; z.nastavCitatel(10); z.nastavJmenovatel(0); try { cout << "10 / 0 = " << z.vydel() << endl;
} catch(Vyjimka v) { // pokud se vrati vyjimka Vyjimka cout << v->vratDuvod() << endl; } catch(Jina vyjimka j) { // pokud vyvolana jina vyjimka... ... throw; // opetovne uvolneni vyjimky pro zpracovani }
31
32
Práce s řetězci, soubory a výjimky
Jeden ze smyslů výjimek spočívá v tom, že nespoléháme na informovanost uživatele (programátora), že může dojít k určitému typu chyby (např. že se nesmí dělit nulou nebo že při TCP spojení může dojít k určitému typu chyby) a že tuto situaci ošetří vhodnou podmínkou. V případě využití výjimky stačí, aby uživatel vědel, že operace je za určitých okolností chybná a dal ji do bloku try a definoval jeho ošetření. O zbytek se postará mechanismus výjimek, který zamezí pádu programu.
Hierarchie výjimek Při vytváření výjimek se často využívá dědičnost. Zjednodušuje jejich zpracování (např. všechny výjimky mají jednu určitou metodu pro vrácení důvodu) a samozřejmě zjednodušuje jejich tvorbu (definuju pouze změny). Tato hierarchie je do značné míry dodržována i ve výjimkách nástrojů Standardní šablonové knihovny. Taková malá hierarchie našich uživatelských výjimek by mohla vypadat tato:
class Vyjimka { private: string text; public: Vyjimka(string s){ text = s;
} string vratDuvod() { return text;
} }; class DeleniNulou : public Vyjimka{ private: int cislo; public: DeleniNulou(string s, int i):Vyjimka(s){ cislo=i;
} int vratPuvodnihoJmenovatele() { return cislo;
} };
Při odchytávání hierarchických výjimek je důležité odchytávat je ve správném pořadí. Catch blok pro odchycení potomka odchytí i předka! Překladač Vás však na tento problém obvykle upozorní.
Kromě catch bloku pro odchytávání určitého typu výjimek lze napsat i catch, který odchytí jakoukoliv výjimku. Stačí do závorek napsat místo typu výjimky tři tečky - catch(...) { }.
Práce s řetězci, soubory a výjimky
Neodchycené a neočekávané výjimky Podívejme se, co nastane za situaci, když zapomene odchytit určitou výjimku. Výjimka hledá odpovídající blok catch. Pokud ho nenajde do konce metody (funkce) vyskočí do metody odkud byla tato zavolána a opět hledá svůj catch. Tímto způsobem může výjimka vyskočit až do funkce main. Pokud ani zde není odchycena, zavolá se funkce terminate. Ta, pokud není řečeno jinak, vypíše chybovou hlášku a zavolá abort. Abort nastaví programu návratovou hodnotu 3 a ukončí ho. Chování funkce terminate můžeme ručně změnit.
void mojeTerminate() { cerr <<"Nesetrena vyjimka"<<endl; exit(10); // radeji vzdy ukoncit program
} int main() { set terminate(mojeTerminate); throw 1; cout << "Nikdy se neprovede" << endl; return 0;
} Obdobná sitace nastává pokud je vyvolána neočekávaná výjimka. V tomto případě se zavolá funkce unexpected. Ta, pokud není řečeno jinak, zavolá terminate. Opět lze její chování modifikovat.
void mojeUnexp() { cerr <<"Neocekavana vyjimka"<<endl; exit(10); // radeji vzdy ukoncit program
} int main() { set unexpected(mojeUnexp); ...
}
Zamezení vyhození výjimky Někdy může být smysluplné zabrát automatickému vyhození výjimky (např. ošetřujeme tento případ ručně). V tom případě využijeme klíčového slovat nothrow.
Např.: zabraňte vyhození výjimky v případě, že se příkazu new nepovede zaalokovat paměť.
i = new(nothrow) int[10];
Příklady Příklad 1 Vytvořte program, který bude realizovat jednoduchou hru - přidávání věží na šachovnici. Na šachovnici lze vkládát věže pouze na neohrožená pole, tj. na pole, kde na stejném řádku nebo sloupci již nestojí jiná věž. Naprogramujte jednoduché textové menu, které bude umožňovat vkládání věží a ukládání, resp. nahrávání věží do/ze textového souboru.
33
34
Práce s řetězci, soubory a výjimky Řešení:
#include #include using namespace std; class Sachovnice{ private: int sachovnice[8][8]; string jmenoSouboru; public: // vyulujeme sachovnici v konstruktoru // rict priklad s ukladanim nalevo/napravoi Sachovnice(char jmeno[]){ jmenoSouboru = jmeno; for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } } // tato metoda umoznuje pridat vez // vrati 0, pokud dane misto neni ohrozeno a 1 pokud ano int pridejVez(int x, int y){ bool jinaVeSloupci = false; bool jinaVRadku = false; // je vubec vez na sachovnici? if (x<8 and y<8 and x>=0 and y>=0){ for(int i=0; i<8; i++) if (sachovnice[i][y] != 0) jinaVeSloupci = true; for(int i=0; i<8; i++) if (sachovnice[x][i] != 0) jinaVRadku = true; if (not(jinaVRadku) and not(jinaVeSloupci)){ sachovnice[x][y] = 1; return 0; } else return 1; // ohrozena pozice } else { // konec ifu return 2; // polozeni mimo rozsah
} } // konec metody void vypisSachovnici(){ for (int i=0; i<8; i++) { for (int j=0; j<8; j++) cout << sachovnice[i][j] << " "; cout << endl;
} } // testovaci metoda pro kontrolu spravnosti implemenatce tridy int test(){ // vynuluj pred testy, jen pro jistotu, ze je prazdna for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } // kontrola prekroceni rozsahu
Práce s řetězci, soubory a výjimky
bool chyba = false; if (pridejVez(5,5) != 0){ cerr << "nejde vlozit na korektni souradnici" << endl; chyba = true; } else { if (pridejVez(7,5) == 0){ cerr << "vlozil jsem vez na pole s obsazenou y souradnici" << endl; chyba = true;
} if (pridejVez(5,7) == 0){ cerr << "vlozil jsem vez na pole s obsazenou x souradnici" << endl; chyba = true;
} } if (pridejVez(-8,1) != 2){ cerr << "neni dobre osetren rozsah, zaporne souradnice na x" << endl; chyba = true;
} if (pridejVez(2,-9) != 2){ cerr << "neni dobre osetren rozsah, zaporne souradnice na y" << endl; chyba = true;
} if (pridejVez(8,3) != 2){ cerr << "neni dobre osetren rozsah, velke souradnice na x" << endl; chyba = true;
} if (pridejVez(4,10) != 2){ cerr << "neni dobre osetren rozsah, velke souradnice na y" << endl; chyba = true;
} // vynuluj i po testech for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } if (chyba) return 1; else return 0;
} void vypisMenu(){ cout << endl << ".:: Menu programu ::." << endl; cout << "1.. vlozeni veze" << endl; cout << "2.. ulozeni do souboru" << endl; cout << "3.. nacteni ze souboru" << endl; cout << "9.. ukonceni hry" << endl;
} int ulozDoSouboru(){ ofstream out; out.open(jmenoSouboru.c str()); if (out.is open()){ for (int i=0; i<8; i++) { for (int j=0; j<8; j++) out << sachovnice[i][j] << " "; out << endl;
} } else return 1;
35
36
Práce s řetězci, soubory a výjimky
return 0;
} int nactiZeSouboru(){ ifstream in; in.open(jmenoSouboru.c str()); if (in.is open()){ for (int i=0; i<8; i++) for (int j=0; j<8; j++) in >> sachovnice[i][j]; } else return 1; return 0;
} }; int main (int argc, char* argv[]) { if (argc == 2) { Sachovnice* deska = new Sachovnice(argv[1]); // insert code here... int x = 0; int y = 0; int volba = 0; while(volba != 9){ deska->vypisMenu(); cout << "Zadej volbu: "; cin >> volba; switch(volba){ case 1: cout << "zadej x: "; cin >> x; cout << "zadej y: "; cin >> y; if (deska->pridejVez(x,y) == 0){ cout << "Vez pridana" << endl; deska->vypisSachovnici(); } else cerr << "Vez nelze pridat" << endl; break; case 2: if (deska->ulozDoSouboru() == 0){ cout << "Soubor ulozen" << endl; } else { cerr << "Sobor nelze ulozit" << endl;
} break; case 3: if (deska->nactiZeSouboru() == 0){ cout << "Soubor nacten" << endl; deska->vypisSachovnici(); } else { cerr << "Sobor nelze nacist" << endl;
} break; case 9: cout << "program konci" << endl; break; default: cout << "zadana neplatna volba" << endl; } // konec switche } // konec celeho cyklu while delete(deska); } else
Práce s řetězci, soubory a výjimky
cerr << "Spatny pocet parametru" << endl; return 0;
}
Příklad 2 Rozšiřte předcházející příklad o ukládání do binárního souboru a načítání z něho. Zamyslete se nad obtížností naprogramování uložení do binární a textového souboru. Řešení:
#include #include using namespace std; class Sachovnice{ private: int sachovnice[8][8]; string jmenoSouboru; public: // vyulujeme sachovnici v konstruktoru // rict priklad s ukladanim nalevo/napravoi Sachovnice(char jmeno[]){ jmenoSouboru = jmeno; for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } } // tato metoda umoznuje pridat vez // vrati 0, pokud dane misto neni ohrozeno a 1 pokud ano int pridejVez(int x, int y){ bool jinaVeSloupci = false; bool jinaVRadku = false; // je vubec vez na sachovnici? if (x<8 and y<8 and x>=0 and y>=0){ for(int i=0; i<8; i++) if (sachovnice[i][y] != 0) jinaVeSloupci = true; for(int i=0; i<8; i++) if (sachovnice[x][i] != 0) jinaVRadku = true; if (not(jinaVRadku) and not(jinaVeSloupci)){ sachovnice[x][y] = 1; } else return 1; // ohrozena pozice } else { // konec ifu return 2; // polozeni mimo rozsah
} return 0;
} // konec metody void vypisSachovnici(){ for (int i=0; i<8; i++) { for (int j=0; j<8; j++) cout << sachovnice[i][j] << " "; cout << endl;
} }
37
38
Práce s řetězci, soubory a výjimky
// testovaci metoda pro kontrolu spravnosti implemenatce tridy int test(){ // vynuluj pred testy, jen pro jistotu, ze je prazdna for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } // kontrola prekroceni rozsahu bool chyba = false; if (pridejVez(5,5) != 0){ cerr << "nejde vlozit na korektni souradnici" << endl; chyba = true; } else { if (pridejVez(7,5) == 0){ cerr << "vlozil jsem vez na pole s obsazenou y souradnici" << endl; chyba = true;
} if (pridejVez(5,7) == 0){ cerr << "vlozil jsem vez na pole s obsazenou x souradnici" << endl; chyba = true;
} } if (pridejVez(-8,1) != 2){ cerr << "neni dobre osetren rozsah, zaporne souradnice na x" << endl; chyba = true;
} if (pridejVez(2,-9) != 2){ cerr << "neni dobre osetren rozsah, zaporne souradnice na y" << endl; chyba = true;
} if (pridejVez(8,3) != 2){ cerr << "neni dobre osetren rozsah, velke souradnice na x" << endl; chyba = true;
} if (pridejVez(4,10) != 2){ cerr << "neni dobre osetren rozsah, velke souradnice na y" << endl; chyba = true;
} // vynuluj i po testech for (int i=0; i<8; i++) { for (int j=0; j<8; j++){ sachovnice[i][j] = 0;
} } if (chyba) return 1; else return 0;
} void vypisMenu(){ cout << endl << ".:: Menu programu ::." << endl; cout << "1.. vlozeni veze" << endl; cout << "2.. ulozeni do souboru" << endl; cout << "3.. nacteni ze souboru" << endl; cout << "4.. ulozeni do bin. souboru" << endl; cout << "5.. nacteni z bin. souboru" << endl; cout << "6.. vypis 5 tahu" << endl;
Práce s řetězci, soubory a výjimky
cout << "7.. vypis posl. 5 tahu" << endl; cout << "9.. ukonceni hry" << endl;
} int ulozDoSouboru(){ ofstream out; out.open(jmenoSouboru.c str()); if (out.is open()){ for (int i=0; i<8; i++) { for (int j=0; j<8; j++) out << sachovnice[i][j] << " "; out << endl;
} } else return 1; return 0;
} int nactiZeSouboru(){ ifstream in; in.open(jmenoSouboru.c str()); if (in.is open()){ for (int i=0; i<8; i++) for (int j=0; j<8; j++) in >> sachovnice[i][j]; } else return 1; return 0;
} int ulozDoBinSouboru(){ ofstream bout; bout.open("sachovnice.dat",ios base::binary); if (bout.is open()){ bout.write((char*)sachovnice,sizeof(sachovnice)); bout.close(); } else return 1; return 0;
} int nactiZBinSouboru(){ ifstream bin; bin.open("sachovnice.dat",ios base::binary); if (bin.is open()){ bin.read((char*)sachovnice,sizeof(sachovnice)); bin.close(); } else return 1; return 0;
} int ulozTah(string pokus, int x, int y){ ofstream out; out.open("log.txt", ios base::app); if (out.is open()){ out << pokus << " " << x << " " << y << endl; out.close(); } else return 1; return 0;
} int vypis5Tahu(){ char radek[10]; ifstream in; in.open("log.txt");
39
40
Práce s řetězci, soubory a výjimky
if (in.is open()){ for (int i=0; i<5;i++){ if(in.getline(radek, 10)){ cout << radek << endl; } else return;
} in.close();
} else return 1; return 0;
} int vypisPosl5Tahu(){ char radek[10]; ifstream in; in.open("log.txt"); if (in.is open()){ in.seekg(0, ios::end); int velikost = in.tellg(); // nastane problem pokud uzivatel zada chybnou souradnici // napr. 10, potom se velikost radku zvysi a priklad nebude // fungovat. Lepsi by bylo ukladat jen korektni tahy. int petRadku = 5*6*sizeof(char); if (velikost >= petRadku){ in.seekg(-petRadku, ios::end); for (int i=0; i<5;i++){ in.getline(radek, 10); cout << radek << endl;
} } in.close();
} else return 1; return 0;
} }; int main (int argc, char* argv[]) { if (argc == 2) { Sachovnice* deska = new Sachovnice(argv[1]); // insert code here... int x = 0; int y = 0; int volba = 0; while(volba != 9){ deska->vypisMenu(); cout << "Zadej volbu: "; cin >> volba; switch(volba){ case 1: cout << "zadej x: "; cin >> x; cout << "zadej y: "; cin >> y; if (deska->pridejVez(x,y) == 0){ deska->ulozTah("T", x, y); cout << "Vez pridana" << endl; deska->vypisSachovnici(); } else {
Práce s řetězci, soubory a výjimky
deska->ulozTah("F", x, y); cerr << "Vez nelze pridat" << endl;
} break; case 2: if (deska->ulozDoSouboru() == 0){ cout << "Soubor ulozen" << endl; } else { cerr << "Sobor nelze ulozit" << endl;
} break; case 3: if (deska->nactiZeSouboru() == 0){ cout << "Soubor nacten" << endl; deska->vypisSachovnici(); } else { cerr << "Sobor nelze nacist" << endl;
} break; case 4: if (deska->ulozDoBinSouboru() == 0){ cout << "Soubor ulozen" << endl; } else { cerr << "Sobor nelze ulozit" << endl;
} break; case 5: if (deska->nactiZBinSouboru() == 0){ cout << "Soubor nacten" << endl; deska->vypisSachovnici(); } else { cerr << "Sobor nelze nacist" << endl;
} break; case 6: if (deska->vypis5Tahu() != 0) cout << "Malo tahu" << endl; break; case 7: if (deska->vypisPosl5Tahu() != 0) cout << "Malo tahu" << endl; break; case 9: cout << "program konci" << endl; break; default: cout << "zadana neplatna volba" << endl; } // konec switche } // konec celeho cyklu while delete(deska); } else cerr << "Spatny pocet parametru" << endl; return 0;
}
Příklad 3 Naprogramujte jednoduchý slovní fotbal. Základem bude hráč, který má schopnost odpovědět soupeři na jím vykopnuté slovo. Hráč musí odpovědět slovem, které obsahuje soupeřem řečené slovo (ulehčení oproti běžné verzi této hry). Musí existovat konstrolní metoda, která
41
42
Práce s řetězci, soubory a výjimky zjistí, zda nově zadané slovo bylo korektní a buď předá tah soupeři nebo vynutí opakování zadání. Vyřešte tento příklad nejdříve bez použití výjimek a následně implementujte kontrolu zadaných slov pomocí výjimek. Řešení:
#include using namespace std; class Vyjimka{ private: string duvod; public: Vyjimka(string d){ duvod = d;
} string what(){ return duvod;
} }; class Hrac{ private: string jmeno; int kontrola(string veta, string odpoved) throw (Vyjimka){ if (odpoved.find(veta) == string::npos){ Vyjimka v("Nema podretezec"); throw v;
} return 0;
} public: Hrac(string zadaneJmeno){ jmeno = zadaneJmeno;
} void odpovez(string aktVeta, Hrac* souper){ cout << jmeno << " reaguje na " << aktVeta << endl; cout << "Zadej odpoved: "; char radek[200]; cin.getline(radek,200); string odpoved = radek; try{ kontrola(aktVeta, odpoved); souper->odpovez(odpoved, this); } catch(Vyjimka x) { cout << "Nastala vyjimka: " << x.what() << endl; odpovez(aktVeta, souper);
} /* if (kontrola(aktVeta, odpoved) == 0){ // OK souper->odpovez(odpoved, this); } else { // chyba odpovez(aktVeta, souper);
} */
}
Práce s řetězci, soubory a výjimky
}; class Kopana{ private: Hrac* hrac1; Hrac* hrac2; public: Kopana(string jm1, string jm2){ hrac1 = new Hrac(jm1); hrac2 = new Hrac(jm2); hrac2->odpovez("",hrac1);
} ~Kopana(){ delete(hrac1); delete(hrac2);
} }; int main (int argc, char * const argv[]) { Kopana* hra = new Kopana("karel", "pepa"); delete(hra); return 0;
}
43
44
Knihovna STL a vybrané nástroje OOP
5
Knihovna STL a vybrané nástroje OOP
Datové kontejnery
Šablony jako datové kontejnery Šablony jsou jedním z největších přínosů knihovny STL (Standard Template Library 24 ) navržené firnou SGI. Šablony jsou de facto datové kontejnery bez specifikovaného typu. Např. běžně pracujeme třídou, které obsahuje pole (matici) hodnot typu integer. Dokážeme si určitě představit, že pracujeme např. třídou reprezentující dynamický seznam hodnot typu integer. Šablona je předpis, jak bude taková stuktura vypadat - jak bude uchovávat hodnoty, jaký bude mít konstruktor, jaké metody bude poskytovat. S tím, že není definováno, co bude obsahovat. To je velmi užitečná vlastnost. Představme si, že potřebujeme udělat lineární seznam integerů a následně, že dostaneme za úkol vytvořit stejný seznam floatů (nebo dokonce instancí nějaké třídy). V zásadě budou obě třídy vypadat stejně, ale ve všech atributech a metodách budeme muset dělat úpravy, aby třída pracovala s jiným datovým typem. Tomu se vyhneme právě využitím šablony. Nadefinujeme funkčnost a konkrétní typ dosadíme ”na přání”.
Šablony jsou datové kontejnery reprezentované klasickými třídami bez specifikovaného typu. Typ je do nich dosazován v okamžiku jejich použití.
Podívejme se nejdříme na nejznámějšího zástupce této knihovny - třídu vector.
Třída vector Vector je jednorozměrné dynamické pole. Je to datový kontejner, který se součástí knihovny STL. Jedná se tedy o určitou obecnou šablonu bez specifikovaného typu. Ukažme si použití vectoru na jednoduchém příkladu:
#include #include using namespace std; int main() { // typ, který bude vector uchovávat je v < > vector a(7); // Pole 7 int. čísel, konstruktor přebírá inicializační vel. vector b; // Pole 0 int. čísel cout << "Počet prvků v a je " << a.size() << " "; // voláme klasickou metodu cout << "Počet prvků v b je " << b.size() << endl; // k prvkům vektoru lze přistupovat jako k prvkům pole for(int i = 0; i < a.size(); i++){ a[i] = i + 1; cout << "a[" << i << "] = " << a[i] << endl;
} b = a; //Bez problémů lze použít operátor = return 0;
} 24
http://www.sgi.com/tech/stl/
Knihovna STL a vybrané nástroje OOP
Jak je patrné z předchozího příkladu, z šablony vytvoříme ”klasickou” třídu dosazením typu. V našem případě jsme dosadili typ integer - vector a získali jsme třídu, která přestavuje dynamické pole integerových hodnot.
Vector má dva konstruktory - parametrický, který přebere počáteční velikost vektoru a bezparametrický, který nastaví velikost na nulu. K prvkům lze přistupovat klasicky přes pole[pozice]. Lze s ním tedy pracovat stejně, jako s polem. Kromě toho vector samozřejmě disponuje řadou metod. Jednou z nejvíce využívaných je metoda push back, která přidává prvek na konec pole. Ukažme si několik základních operací:
// deklarace vektoru vector a; // přidávání prvků for(int i = 0; i < 5; i++){ a.push back(i+1); cout << "Posledn’ı prvek je: " << a.back() << endl;
} // průchod polem for(int i = 0; i < a.size(); i++) { cout << a[i] << \t;
} cout << endl; // promazání pole while (!a.empty()) { // opakuj, dokud není pole prázdné a.pop back(); // odeberu poslední prvek
} // kontrola velikosti cout << "Velikost " << a.size() << endl;
Vector sám od sebe neumožňuje vytvářet vícerozměrná pole. Tohoto efektu lze ale dosáhnout zanořením více vectorů do sebe. Prvky výchozího vectoru v takovémto případě nejsou triviální hodnoty a opět vectory. Triviální příklad této konstrukce je předveden v následujícím příkladu:
vector > matice(3); // Matice 3 x 0, mezi > > je mezera! for(int a = 0; a < 3; a++) { matice[a].push back(a+1); matice[a].push back(a+2); matice[a].push back(a+3);
} // Nyní máme matici 3 x 3 for(int y = 0; y < 3; y++){ for(int x = 0; x < 3; x++){ cout << matice[x][y] << ’\t’;
} cout << endl;
}
45
46
Knihovna STL a vybrané nástroje OOP
Podívejme se na tento příklad podrobněji a vysvětleme si, jaký je rozdíl mezi matice.push back(...) a matice[a].push back(...). V prvním případě je nový prvek přidán na konec ”hlavní” matice, tedy té, která jako každý prvek obsahuje vector hodnot. V druhém případě je nový prvek přidáván na konec matice, která je uložena v ”hlavní” matici na pozici a. V prvním případě bych tedy musel vkládat celý vector, v případě druhém se už samozřejmě očekává pouze jedna integerová hodnota. Uvědomit si rozdíl mezi těmito operacemi je zcela nezbytné pro pochopení celé problematiky.
Iterátory Iterátor je de facto ukazatel pro kontejner. Iterátory jsou bezpečnejší než indexy nebo ukazatele. Ke kontejnerům jsou předdefinovány užitečné iterátory na začátek (.begin()), za konec (.end()), aj. Výhodou iterátoru je, že ani při nekorektním použití se nedostane mimo strukturu (nepovede se Vám udělat poce[10] u 5ti prvkové struktury). Iterátory také využívá drtivá většina algoritmů STL, proto je velmi účelné je zvládnout. Ukažme si pro začátek základní operaci - průchod polem:
vector vektor(20); for (vector::iterator temp = vektor.begin(), temp != vektor.end(); temp++){ *temp = 0;
}
Podívejme se na deklaraci iterátoru. Je patrné, že iterátor se vždy váže k typu struktury, nad kterou je definován. Jeho deklarace je tedy vždy typ struktury::iterator. Jak již bylo řečeno, nad vectorem (a většinou dalších struktur) jsou definovány iterátory begin a end. Při průchodu vectorem tedy na počátku nastavím mnou vytvořený iterátor na hodnotu iterátoru begin a postupně jej inkrementuji (operátor ++) až dokud nedosáhne hodnoty iterátoru end. Pokud chci přistoupit na nějakou triviální hodnotu uloženou ve vectoru na pozici na kterou ukazuje iterátor, musím použít konstrukci *název iterátoru (např.: cout << *pozice). Pokud je pod iterátorem uložena instance třídy, použiji operátor -> (např: iterator->objem motoru). Jak bylo řečeno iterátory se používají v mnoha algoritmech a metodách. Ukažme si některé často používané operace s vectorem, které iterátory využívají.
// vložím před první prvek -100 v1.insert(v1.begin(),-100); // vložím na konec 3 krát 500 v1.insert(v1.end(),3,500); // vložím celý vektor v1 do v2 v2.insert(v2.begin()+2,v1.begin(),v1.end()); // mažu prvek pod iterátorem v2.erase(it); // mažu všechny prvky ve v2 v2.erase(v2.begin(), v2.end());
47
Knihovna STL a vybrané nástroje OOP
Prolematika iterátorů je citelně složitější, než je zde uvedeno. Existují dopředné iterátory, výstupní iterátory a řada jiných. Tato problematika, ale přesahuje rámec tohoto kurzu. Pro více podrobností doporučuji navštívit http:/ /www.sgi.com/tech/stl/Iterators.html.
Další šablony v STL Knihovna STL samozřejmě obsahuje řadu dalších šablon, které korespondují s často používanými abstraktními datovými typy. Práce s nimi je naprosto totožná s prací s vectorem. Liší se od sebe obvykle pouze množinou operací, které jsou na nimi definovány. Jejich popisy naleznete na http://www.sgi.com/tech/stl/. Pro přehled uvádím srovnávací tabulku (tabulka je převzdata ze serveru builder.cz - http://www.builder.cz/art/cpp/cppstl.html). Název Typ Hlavičkový Popis kontejneru kontejneru kontejnerusoubor bitset posloupnost bitset Posloupnost bitů pevné délky. deque posloupnost dequeOboustranná fronta. Prvky lze vkládat, nebo odebírat z obou konců. Sice lze rovněž odebírat, nebo vkládat prvky na libovolné místo ve frontě (kontejner deque to umožňuje), ale tato operace není příliš efektivní. list posloupnost list Oboustranně zřetězený seznam. map
asociativní map Asociativní pole. pole, které nemusí být indexováno celočíselným kontejner typem, ale čímkoliv. Třeba řetězcem. Pro daný klíč může existovat pouze 1 asociovaná hodnota. Tomuto kontejneru se budeme v budoucnu zabývat v samostatném článku. multimap asociativní map Asociativní pole. Pro daný klíč (index) může existovat více kontejner asociovaných hodnot. Tomuto kontejneru se budeme v budoucnu zabývat v samostatném článku. multiset asociativní set Multimnožina. množina, ve které se mohou prvky opakovat. kontejner Tomuto kontejneru se budeme věnovat později v samostatném článku. priorityposloupnost queue queuePrioritní fronta. Fronta, ve které neplatí pravidlo ”první dovnitř, první ven”. Prvky, které se do fronty uloží jsou uspořádány podle nějaké relace. Dalo by se říci, že předbíhají ve frontě podle nějaké předem dané priority. queue posloupnost queueKlasické fronta. platí pravidlo, že prvek, který byl jako první vložen do fronty, z ní bude také první vybrán. set asociativní set Množina. Daná hodnota může být v množině obsažena jen kontejner jednou. Tomuto kontejneru se budeme věnovat později v samostatném článku. stack posloupnost stack Klasický zásobník. Platí pravidlo, že prvek, který byl vložen do zásobníku jako poslední bude vybrán jako první. vector posloupnost vectorObdoba jednorozměrného pole. Tomuto kontejneru se budeme věnovat později v samostatném článku.
Pro přehled ještě uvádím tabulku základních operací nad často používanými abstraktními datovými strukturami a jejich formální rozdělení. Operace na základními ADT jednoduché
vector
push back, pop back, insert, erase
list
vector+ push front, pop front
deque
vector+ push front, pop front (pokračování tabulky na další straně)
48
Knihovna STL a vybrané nástroje OOP
kontejnerové adaptéry
asociativní kontejnery
queue
push, front, back
stack
push, top, pop
priority que
push, top, pop
set map mutimap multiset
Algoritmy
Algoritmy knihovny STL Kromě toho, že knihovna STL obsahuje mnoho šablon datových kontejnerů, obsahuje také šablony řady běžně užívaných algoritmů (řazení, vyhledávání, vyplňování, aj.). Než ale o nich začneme mluvit, musíme se seznámit s nástrojem o kterém doposud nebyla řeč - funkčním objektem.
Funkční objekty Funkční objekt je instance třídy, která má jako svou veřejnou metodu operátor (). Je tedy nutné ho přetížit. Závorky se pak chovají jako klasická metoda. Tedy, místo aby jste zavolali metodu, napíšete pouze název instance a za něj závorky s případnými parametry. Funkční objekt pak provede příslušnou operaci popsanou v přetížení. Zcela jistě Vás napadne ”K čemu je taková konstrukce dobrá?”. Odpověď je prostá, jedná se de facto o objektové zapouzdření funkce. Takto vytvořený objekt pak syntakticky plně nahrazuje klasickou funci - volá se stejně (jméno a závorky s parametry). Následující příklad ilustruje velmi jednoduchý funkční objekt vypisující svoje parametry.
class FunkcniTrida { public: int operator()(int parametr) { cout << "volan op. () s par. " << parametr << endl; return parametr * 2;
} }; int main(){ FunkcniTrida objekt; cout << objekt(10) << endl; return 0;
}
Standardní funkční objekty C++ disponuje celou řadou jednoduchých funkčních objektů. Následující výčet uvádí některé často využívané.
• equalto - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Vrací true v případě, že první parametr == druhý parametr.
Knihovna STL a vybrané nástroje OOP
• greater - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). • • • • • • • • • • •
Operátor () vrací proměnnou typu bool. Vrací true v případě, že první parametr > druhý parametr. greater equal - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Vrací true v případě, že první parametr >= druhý parametr. less - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Vrací true v případě, že první parametr < druhý parametr. less equal - Šablona má 1 parametr... not equal - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Vrací true v případě, že první parametr != druhý parametr. logical and - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Operátor () vrací první parametr && druhý parametr. logical or - Šablona má 1 parametr. Parametr udává typ obou parametrů operátoru (). Operátor () vrací proměnnou typu bool. Operátor () vrací první parametr —— druhý parametr. divides - Šablona má 1 parametr. Parametr udává typ obou parametrů a návratové hodnoty operátoru (). Operátor () vydělí své dva parametry. minus - Šablona má 1 parametr. Parametr udává typ obou parametrů a návratové hodnoty operátoru (). Operátor () vrátí rozdíl svých dvou parametrů. modulus - Šablona má 1 parametr. Parametr udává typ obou parametrů a návratové hodnoty operátoru (). Operátor () vrátí zbytek po celočíselném dělení. plus - Šablona má 1 parametr. Parametr udává typ obou parametrů a návratové hodnoty operátoru (). Operátor () sečte své dva parametry. times - Šablona má 1 parametr. Parametr udává typ obou parametrů a návratové hodnoty operátoru (). Operátor () vynásobí své dva parametry.
Algoritmy Nyní máme základní povědomí o funkčních objektech a můžeme se podívat na algoritmy STL, které je využívají. Algoritmy nepracující s datovými kontejnery Podívejme se na velmi jednoduchou ukázku dvou triviálních, ale velmi užitečných nástrojů.
#include #include #include using namespace std; less pravidlo; int main() { int a = 1, b = 2, c = 3; cout << min(a,b) << endl; cout << max(a,c) << endl; swap(a,c); cout << min(a,b,pravidlo) << endl; return 0;
}
Již z názvu je jasné, co asi nástroje min, max a swap dělají, proto se zaměřím na poslední příklad použití funkce min. Jako třetí nepovinný parametr je uveden funkční objekt pravidlo. Pravidlo je instance šablony less, která vrací menší ze dvou čísel (viz výše). Místo šablony
49
50
Knihovna STL a vybrané nástroje OOP
less bychom mohli dosadit vlastní funkci nebo funkční objekt, který by porovnával např. dvě auta a vracel lehčí z nich. Nyní se ale podívejme na velkou a často používanou množinu nástrojů pracujících s datovými kontejnery.
Algoritmy pro práci s datovými kontejnery První skupinu těchto algoritmů bychom mohli nazvat algoritmy pro vyplňování kontejnerů. V C++ existují 4 algoritmy, kterými lze vyplňovat kontejnery určitými hodnotami. Pro vyplnění kontejneru konstantní hodnotou používáme fill a fill n. Parametry fill jsou iterátory na začátek a konec a prvek, který se má vložit mezi prvky. fill n má 3 parametry - iterátor udávající začátek, počet prvků a vkládaný prvek. Následující příklady demonstrují použití těchto nástrojů.
#include #include #include #include using namespace std; int main(){ vector vektor(10,0); //10x0 for(vector::iterator i = vektor.begin(); i != vektor.end(); i++) cout << *i << "\t"; cout << endl; fill(vektor.begin(), vektor.end(), 20);
}
fill n(vektor.begin()+3, 3, 100); // vektor[3] až vektor[5] vlož 100. ofstream soubor("Pokus.txt"); // include fill n(ostream iterator(soubor,","),10,0); // do textového souboru "Pokus.txt" zapiš // deset nul oddělených čárkou. soubor.close();
Dlaším krokem je vyplňování nekonstantní hodnotou. Pro tento účel máme k dispozici algoritmy generate a generate n. Jejich použití je obdobné jako u fill - místo parametru vkládaného prvku je parametrem třída funkčního objektu nebo funkce (generátoru) - tj. očekává se ukazatel na funkci, která nemá parametry a vrací typ prvku v kontejneru nebo funkční objekt jehož třída má přetížen operátor () tak, aby vracel typ prvku v kontejneru a neměl parametry (viz výše).
Knihovna STL a vybrané nástroje OOP
// třída ze které vytvoříme funkční objekt class Faktorial{ private: int i, vysledek; public: Faktorial(){ i = 0; vysledek = 1;
} int operator() () { i++; return vysledek *=i;
} }; Faktorial f; // funkční objekt generate(vektor.begin(),vektor.end(),f);
generate(vektor.begin(),vektor.end(),rand); // vyplní náhodnými čísly z funkce rand int pole[20]; generate n(pole, 20, rand);
Další často užívanou skupinou algoritmů jsou algoritmy pro řazení. Pro řazení můžeme použít C funkci qsort, umí ale pracovat pouze s polem. Lépe je používat sort nebo stable sort. Ty umí pracovat jak s klasickým polem, tak ADS nad kterými jsou definovány iterátory.
Stabilní řazení je řazení, které garantuje, že prvky, které mají stejný klíč (podle kterého se prvky řadí) vůči sobě nezmění pořadí. U ”nestabilního” řazení může být tato vlastnost také splněna, ale nemáme jistotu, že tomu tak bude vždy.
51
52
Knihovna STL a vybrané nástroje OOP
#include #include #include #include using namespace std; int main(){ vector vektor(5,0); vektor[1] = 3; vektor[3] = 6; cout << "Nesetrideny vektor: "; for(vector::iterator i = vektor.begin(); i != vektor.end(); i++) cout << *i << " "; cout << endl; // sestupne sort(vektor.begin(),vektor.end(), less()); cout << "Setrideny vektor: "; for(vector::iterator i = vektor.begin(); i != vektor.end(); i++) cout << *i << " "; cout << endl; // vzestupne int pole[5] = {1, 80, -87, 25, 0 }; sort(&pole[1],&pole[5]); cout << "Setridene pole: "; for(int i=0; i<5; i++) cout << pole[i] << " "; cout << endl; return 0;
}
Poslední skupinou algoritmů o kterých zde budu psát jsou skenovací algoritmy. Skenovací algoritmy prochází kontejnery, zjišťují jejich obsah a následně (s ním) něco dělají. Příkladem může být alg. count nebo count if. Alg. zjišťují počet buněk odpovídajících určitému kritériu. Dalším příkladem může být alg. accumulate, který umožňuje prvky sčítat, násobit, atp. Podívejme se na příklad použití algoritmu count.
Knihovna STL a vybrané nástroje OOP
#include #include #include using namespace std; bool podminka(int a){ return (20 < a) && (a < 80);
} int main(){ int pole[10] = { 12, 80, 3, 5, 2, 6, 2, 0, 9, 10 }; vector vektor(pole, &pole[10]); // kopie pole int pocet1 = 0; int pocet2 = 0; pocet1 = count(vektor.begin(), vektor.end(), 80); pocet2 = count if(pole, &pole[10], podminka); ...
}
Obdobným způsobem pracuje i algoristmus accumulate. Příklad níže ukazuje aplikaci accumulate jak na vector, tak klasické pole.
#include #include #include using namespace std; int main(){ cout << "Součet všech prvků je: "; int soucet = accumulate(vektor.begin(), vektor.end(), 0); // 0 je počáteční hodnota cout << soucet << endl; cout << "Součin prvních 3 prvků :"; int soucin = accumulate(pole, &pole[3], 1, times()); cout << soucin << endl;
}
Často se lze setkal s mylným tvrzením (při srovnávání s jinými jazyky), že v C++ neexistuje for each, nebo nějaká jeho obdoba. for each umí pracovat opět jak s polem, tak s kontejnery. Následující příklad ukazuje aplikaci for each na klasické pole.
void funkce(int a) { cout << "Je volána funkce s parametrem " << a << endl;
} int main(){ int pole[7] = {1 , 2, 100, 23, 43, 56, 75 }; for each(pole,&pole[7],funkce); ...
Poslední příklad je použití for each v kombinaci s vektorem. V praxi velmi časté použití. Aby byl příklad reálnější, nepracujeme s vectorem triviálních typů, ale instancí třídy Bod.
53
54
Knihovna STL a vybrané nástroje OOP
class Bod{ private: int x,y; public: Bod(int a, int b) { x = a; y = b; } void nastavX(int value) { x = value; } void nastavY(int value) { y = value; } int vratX() { return x; } int vratY() { return y; } }; class PosunODeset{ public: void operator()(Bod &b){ b.nastavX(b.vratX() + 10); b.nastavY(b.vratY() + 10);
} }; int main(){ vector body; Bod b1(0,0), b2(10,10), b3(-100, 1000), b4(10,7); body.push back(b1); body.push back(b2); body.push back(b3); body.push back(b4); // Teď posuneme body o 10 jednotek na ose x i y PosunODeset posun(); for each(body.begin(),body.end(),posun);
}
Vybrané nástroje OOP
Vybrané nástroje OOP Tato závěrečná část kapitoly se věnuje několika tématům, které doposud nebyly zmíněny, ale pro všeobecný přehled je vhodné je znát. První nepopsaným tématem jsou prostory jmen.
Prostory jmen Třídy, ale i proměnné a případně další nástroje C++ lze třídit do skupin - prostorů jmen. Smyslem je vyhnout se duplictním jménům. Nemá smysl využívat je u triviálních aplikací, proto jim doposud nebyla věnována pozornost. Programy, které jsme dosud tvořili byly natolik krátké, že nemělo smysl do nich prostory jmen zavádět. V některých jazycích (Java, C#, aj.) jsou ale prostory jmen nedílnou součástí každého programu. V C++ je zpravidla využíváme při tvorbě knihoven nebo složitých aplikací. Pro identifikaci jmenného prostoru používáme operátor :: nebo deklaraci using namespace. Existuje také implicitní prostor jmen, který nemá jméno. Podle ANSI C++ existuje standardní prostor jmen std, tam patří mnoho běžně užívaných nástrojů (cin, cout, aj.). Následující program ukazuje použití funkce, která spadá pro implicitního prostoru jmen a funkcí spadajících do prostoru jmen std. Protože jsme nepoužili deklaraci using namespace, musíme uvádět název prostoru std a ”čtyřtečku” před každou funkcí z tohoto prostoru.
Knihovna STL a vybrané nástroje OOP
int secti(int a, int b) { return a + b + 1;
} int main(void) { std::cout << "Ahoj svete" << std::endl; std::cout << secti(2,3) << std::endl; return 0;
}
Následující kód ilustruje, jak vytvořit vlastní prostor jmen a jak následně přistoupit k jeho členům. Zavedeme nový prostor jmen a do něj vložíme funkcí sečti, následně funkci se stejným názvem vytvoříme v implicitním jmenném prostoru.
namespace JmennyProstor{ class Trida { private: int atribut; public: void metoda(){ std::cout << "Ahoj" << std::endl;
} }; int secti(int a, int b){ return a + b;
} } int secti(int a, int b) { return a + b + 1;
} int main(void) { cout << secti(2,3) << endl; cout << JmennyProstor::secti(2,3) << endl; JmennyProstor::Trida* instance; instance = new JmennyProstor::Trida; instance->metoda(); delete(instance); return 0;
}
Členské proměnné a metody Veškeré atributy, které jsme doposud používali byli atributy objektů - byli unikátní pro jednotlivé objekty. V C++ můžeme definovat i atributy, které náleží přímo třídám, označujeme je zpravidla jako členské proměnné. Pro deklaraci atributů třídy (členských proměnných) používáme klíčové slovo static. Ke čl. prom. lze přistupovat pomocí názvu třídy, :: a názvu proměnné (Třída::proměnná).
55
56
Knihovna STL a vybrané nástroje OOP
class Trida{ private: static int pocetInstanci; // nelze napsat = 0; int normaniAtribut; ...
Tak jako definujeme členské proměnné, můžeme mít i členské metody. Členská metoda (označena static) nemá jako svůj první implicitní parametr this. Je to logické, protože není jasné pro který objekt je volána (není parametr this).
static int vratPocetInstanci() { return pocetInstanci;
}
V těle statické (členské) metody lze pracovat jen se statickými atributy a metodami třídy (členské prom. a metody). Lze ji také zavolat, aniž by existovala nějaká instance dané třídy.
Jiné využití static
• Je-li static globální proměnná (nebo funkce), je viditelná pouze v daném souboru zdrojového textu (modulu).
• Je-li static lokální proměnná funkce, potom její hodnota je uchovávána mezi jednotlivým voláním (zaniká při ukončení programu) Ukažme si klasický příklad na využití popsané problemtiky - počítadlo instancí dané třídy.
Knihovna STL a vybrané nástroje OOP
#include using namespace std; class Trida { private: static int pocetInstanci; int atribut; public: Trida() { atribut = 0; pocetInstanci++;
} ~Trida() { pocetInstanci{;
} static int vratPocetInstanci() { // je i const return pocetInstanci;
} int normalniMetoda(int a){ atribut = a; return atribut;
} }; int Trida::pocetInstanci = 0; //inicializace statickeho atr. int main() { Trida objekt1, objekt2; Trida* objekt3 = new Trida(); cout << Trida::vratPocetInstanci() << endl; cout cout cout cout
<< << << <<
objekt2.normalniMetoda(10) << endl; objekt1.normalniMetoda(10) << endl; objekt3->normalniMetoda(10) << endl; objekt3->normalniMetoda(5) << endl;
delete objekt3; cout << Trida::vratPocetInstanci() << endl;
//const Trida objekt4; //cout << objekt4.vratPocetInstanci(); return 0;
}
Klíčové slovo const • Pokud jej aplikujeme na proměnnou, vytvoříme klasickou konstantu (const int pocet = 10;). Jedná se o pružnější obdobu #define.
• Pokud píšeme const za metodou, říkáme, že metoda nemění stav objektu (int vratHodonotuAtributu() const;).
• V C++ lze vytvořit i konstantní instanci, u které nelze nijak měnit vnitřní stav. U takové instance lze volat pouze metody deklarované s klíčovým slovem const.
57
58
Knihovna STL a vybrané nástroje OOP
Kopírovací konstruktory
Často potřebujeme kopírovat objekty stejně jako triviální typy (int b = a;). Bohužel to nejde automaticky. Na první pohled by se mohlo zdát, že to půjde zhruba takto: Trida* instance = new Trida; Trida* jinaInstance = instance; Toto však není kopie. Oba odkazy ukazují na jedno paměťové místo. Je to zřejmé, pokud si kód rozepíšeme. Trida* instance; // vytvoř ukazatel do paměti instance = new Trida; // vytvoř instanci a ulož ji na pozici ukazatele Trida* jinaInstance; // vytvoř nový ukazatel do pam. jinaInstance = instance; // nastav ukazatel na stejnou pozici jako prvni uk. Pokud chceme docílit opravdové kopie, musí být pro danou třídu implementován kopírovací konstruktor. Typy kopie objektu:
• Plytká - zkopírují se hodnoty jednotlivých ukazatelů a nestará se o paměť. • Hluboká - zkopíruje vše, i bloky paměti, na které ukazují ukazatele.
Není-li definován kopírovací konstruktor explicitně (ručně), je použit implicitní kopírovací konstruktor. Implicitní kopírovací konstruktor vytváří vždy plytkou kopii. KK má vždy následující syntaxi: Třída(const Třída& vzor). Podstatné je uvědomit si, kdy se mám o kopírovací konstruktor začít starat. Na to platí jednoduché pravidlo:
Pokud kopírujete objekt, který má alespoň v jednom atributu ukazatel, je naprosto nezbytné, starat se o kopírovací konstruktor.
Ukažme si tento problém na poněkud delším příkladu. Mějme dvě třídy. Třídu Hráč a třídu Šachovnice. Obě budou poměrně triviální. Hráč bude obsahovat jméno a šachovnice vector vectorů intů, který bude reprezentovat hrací pole.
Knihovna STL a vybrané nástroje OOP
class Hrac{ private: string jmeno; public: Hrac(string j){ jmeno = j;
} string vratJmeno(){ return jmeno;
} }; class Sachovnice{ private: vector< vector > deska; public: Sachovnice(){ vector sloupec(8); // vynulovat for(int i=0; i<8; i++){ deska.push back(sloupec);
} } void vypisDesku(){ cout << endl << "Deska " << endl; for(int i=0; i<deska.size(); i++){ for(int j=0; j<deska[i].size(); j++) cout << deska[i][j] << " "; cout << endl;
} } void nastav(int x, int y, int value){ deska[x][y] = value;
} int vrat(int x, int y){ return deska[x][y];
} };
Nyní si nadeklarujeme třídu šachy, která bude reprezentovat jednu šachovou partii. Šachy budou hrát dva hráči a ke hře bude náležet určitá šachovnice.
59
60
Knihovna STL a vybrané nástroje OOP
class Sachy{ private: Sachovnice* deska; Hrac* prvni; Hrac* druhy; public: Sachy(string jmeno1, string jmeno2){ deska = new Sachovnice(); prvni = new Hrac(jmeno1); druhy = new Hrac(jmeno2);
} void vypisDesku(){ deska->vypisDesku();
} void nastav(int x, int y, int value){ deska->nastav(x, y, value);
} // Bez toho kodu to bude havarovat Sachy(const Sachy& vzor){ deska = new Sachovnice(); prvni = new Hrac(vzor.prvni->vratJmeno()); druhy = new Hrac(vzor.prvni->vratJmeno()); for(int i=0; i<8; i++) for(int j=0; j<8; j++) deska->nastav(i, j, vzor.deska->vrat(i,j));
} ~Sachy(){ delete(deska); delete(prvni); delete(druhy);
} };
Poslední částí našeho programu je již triviální funkce main.
int main (int argc, char * const argv[]) { Sachy* hra = new Sachy("Karl", "Egon"); Sachy*kopie = new Sachy(*hra); hra->nastav(2,2,9); hra->vypisDesku(); kopie->vypisDesku(); delete(hra); delete(kopie); return 0;
}
Zkusme se nyní nad programem zamyslet. Co je řečeno v hlavní funkci programu? 1) Vytvoř šachovou partii s hráči Karl a Egon. 2) Udělej kopii této partie. Pokud bychom se
Knihovna STL a vybrané nástroje OOP pokusili udělat kopii bez ručního nadefinování parametrického konstruktoru nastaly by hned dva problémy. 1) Obě instance hry by sdílely dva stejné hráče a jednu stejnou šachovnici. Tedy pokud bych změnil šachovnici v instanci hra, změnila by se i v instanci kopie. To je první zásadní problém. Bez explicitního kop. konstruktoru by se zavolal jen implicitní kopírovací konstruktor a ten by kopíroval pouze hodnoty v atributech, tedy kopíroval hodnoty ukazatelů (kam ukazují, ne už na co ukazují). Druhý zásadní problém nastane při rušení hry, resp. při rušení její kopie. Uvědomte si, že pokud zruším hru, zruší se korektně i vše, co hra obsahuje, tedy i hráči a šachovnice. V tom okamžiku (pokud nemám nadefinovaný expl. k.k.) se zruší právě ta šachovnice a ti hráči na které se odkazuje i kopie. Tedy kopie přestává být funkční. Navíc, pokud se pokusím kopii zrušit, pokusí se zrušit i již neexistující agregované objekty, což pravděpodobně vyvolá chybu.
Systém Ms Windows je k chybám tohoto typu poměrně benevolentní, takže se běžně stává, že systém tuto chybu nezachytí, pokud uvolněnou paměť nevyužívá nějaká klíčová funkce systému nebo aplikace. Jedná se však o hrubou a nebezpečnou chybu a je nutné se jí vyvarovat.
V případě jako je tento je tedy evidentně nezbytné, aby byl nadefinován explicitní kopírovací konstruktor, který se postará nejen o správné nastavení ukazatelů, ale i o kopírování objektů, na které ukazalete ukazují. Pokud jste se v předcházející příkladu ”ztratili”, zde je krátký příklad ilustrující stejný problém.
61
62
Knihovna STL a vybrané nástroje OOP
#include using namespace std; class Pole { private: public: int delka; int* pole; public: Pole(int delka){ this->delka = delka; pole = new int[delka]; for(int i = 0; i < delka; i++) pole[i] = i;
} /* Pole(const Pole& P){ delka = P.delka; pole = new int[delka]; for(int i = 0; i < delka; i++) pole[i] = P.pole[i];
} */ ~Pole() { delete pole;
} void vypisPole(){ for(int i = 0; i < delka; i++) cout << pole[i] << " "; cout << endl;
} }; int main() { Pole a(7); a.vypisPole(); Pole b(a); // použije se kopírovací konstruktor //Pole b = a; // opet se pouzije kk a.pole[2]=20; a.vypisPole(); b.vypisPole(); return 0;
}
Pokud se program chová ”podivně”. I původně funkční kód najednou způsobuje pády programu, je jednou z častých (a těžko hledatelných) příčin právě absence kopírovacího konstrutoru.
Knihovna STL a vybrané nástroje OOP
Zkuste si výše uvedené příklady zkopírovat do Vašeho vývojového prostředí a zkuste, jak se budou chovat s kopírovacím konstruktorem a bez něj.
Inline funkce Při volání metod vzniká nezanedbatelná režie - vyhodnocení parametrů, uložení na zásobník, ... U krátkých funkcí může být režie větší, než samotný výpočet. Klíčové slovo inline říká překladači, že místo skoku do podprogramu je má na místo volání dát tělo funkce. Tato deklarace není pro překladač závazná.
inline int soucet(int a, int b){ return a+b;
}
struct vs. class • • • •
struct (struktura) znamená v praxi téměř přesně totéž, co class. U stuct, pokud není řečeno jinak, jsou složky veřejné. Struktura může být předkem třídy a opačně. Při deklaraci třídy můžeme tato slova zaměnit (tj. můžeme deklarovat strukturu a implementovat ji jako třídu a opačně).
struct Trida; class Trida{ ... };
Používání struktur (struct) je poměrně časté. Nalezneme je i v řadě fundovaných knih (např. od B. Eckela), osobně však nevidím žádnou zásadní výhodu v jejich používání. Jední ze základních principů OON je uzavřenost tříd a tu splňují lépe klasické třídy u kterých je defaultní modifikátor private. Podle mého názoru je kombivání struktur a tříd v kódu minimálně pro začínající programátory poměrně nešťastné.
Příklady Příklad 1 Mějme následující úkol. Vytvořte vlastní třídu, která bude reprezentovat dynamické 2D pole. Navrhněte a implementuje metody, které budou realizovat přidání a odebrání sloupce, resp. řádku. Vnitřní reprezentace matice bude vector vectorů hodnoty typu integer. Situace, kdy musíte definovat vlastní datový typ a skupinu operací na ním je v praxi poměrně častá, takže doporučuji tomuto příkladu věnovat pozornost.
63
64
Knihovna STL a vybrané nástroje OOP Řešení:
#include #include using namespace std; class DynPole{ private: vector< vector > matice; int x; int y; public: DynPole(int x, int y){ vector radek(y); for(int i=0; ix = x; this->y = y;
} void vypisPole(){ for(int i=0; i<x; i++){ for(int j=0; j
} } void pridejRadek(){ vector radek(y); for(int i=0; i
} void pridejRadek(int count){ for(int i=0; i
} void pridejSloupec(){ for(int i=0; i<x; i++) matice[i].push back(0); y++;
} void pridejSloupec(int count){ for(int i=0; i
} void vloz(int x, int y, int value){ if (x>=0 && xx && y>=0 && yy) matice[x][y] = value;
} int vrat(int x, int y){ if (x>=0 && xx && y>=0 && yy) return matice[x][y]; return -1;
Knihovna STL a vybrané nástroje OOP
} void odeberRadek(){ if(x>0){ matice.pop back(); x{;
} } void odeberRadek(int count){ if (count > x) // neni nutne count = x; for(int i=0; i
} void odeberSloupec(){ if (y>0){ for(int i=0; i<x; i++) matice[i].pop back(); y{;
} } void odeberSloupec(int count){ if (count > y) // neni nutne count = y; for(int i=0; i
} }; int main () { DynPole* pole = new DynPole(3,5); pole->pridejRadek(2); pole->pridejSloupec(2); pole->vypisPole(); delete(pole); DynPole policko(3,5); pole.pridejRadek(); return 0;
}
Příklad 2 Příkladem, který bychom si mohli uvést na závěr kapitoly o algoritmech je setřídění vectoru netriviální chodnot. Například bodů. Bod bude mít souřadnice x a y. Dostaneme za úkol vector těchto bodů setřídit podle souřadnice x. Budeme se muset udělat vlastní funkci nebo funkční objekt, který bude body porovnávat. Pro výpis setříděného vectoru bychom pak mohli využít funci for each. Řešení:
#include #include #include #include using namespace std; class Bod{ private: int x,y; public: Bod(int a, int b) {
65
66
Knihovna STL a vybrané nástroje OOP
x = a; y = b;
} int vratX() { return x;
} int vratY() { return y;
} }; // funkce, ktera slouzi pro vypis bodu pole void vypisPrvek(Bod bod){ cout << "X: " << bod.vratX() << endl; cout << "Y: " << bod.vratY() << endl; cout << endl;
} // borovnani bodu class MensiBod{ public: bool operator()(Bod prvni, Bod druhy){ return prvni.vratX() < druhy.vratX();
} }; class Pole{ private: vector pole; public: void pridejBod(Bod bod){ pole.push back(bod);
} void seradPole(){ MensiBod mensi; sort(pole.begin(),pole.end(), mensi);
} void vypisPole(){ for each(pole.begin(),pole.end(),vypisPrvek);
} int soucetRadku(int radek){ int sum = 0; for (vector::iterator it = pole.begin(); it != pole.end(); it++){ if (it->vratX() == radek) sum += it->vratY();
} return sum;
} }; int main(){ Pole* namereno = new Pole; int x = 1, y = 1; while ((x!=0) or (y!=0)){ cout << "Zadej X a Y: "; cin >> x; cin >> y; Bod bodik(x,y); namereno->pridejBod(bodik);
} namereno->vypisPole(); cout << "Serazene pole" << endl;
Knihovna STL a vybrané nástroje OOP
namereno->seradPole(); namereno->vypisPole(); cout << "Soucet radku je: " << namereno->soucetRadku(2); delete(namereno); return 0;
}
Příklad 3 Tento příklad je rozebrán i v oporách, zde jej uvádím v ”plném znění”. Mějme dvě třídy. Třídu Hráč a třídu Šachovnice. Obě budou poměrně triviální. Hráč bude obsahovat jméno a šachovnice vector vectorů intů, který bude reprezentovat hrací pole. Dále si nadeklarujeme třídu šachy, která bude reprezentovat jednu šachovou partii. Šachy budou hrát dva hráči a ke hře bude náležet určitá šachovnice. Vytvořte v hlavní funkci programu novou šachovou partii a pokuste se provést její kopii. Pokud nastne chyba (buďte si jisti, že nastane) a nová šachovnice nebude samostatná, implementujte kopírovací konstruktor. Řešení:
#include #include using namespace std; class Hrac{ private: string jmeno; public: Hrac(string j){ jmeno = j;
} string vratJmeno(){ return jmeno;
} }; class Sachovnice{ private: vector< vector > deska; public: Sachovnice(){ vector sloupec(8); // vynulovat for(int i=0; i<8; i++){ deska.push back(sloupec);
} } void vypisDesku(){ cout << endl << "Deska " << endl; for(int i=0; i<deska.size(); i++){ for(int j=0; j<deska[i].size(); j++) cout << deska[i][j] << " "; cout << endl;
} } void nastav(int x, int y, int value){ deska[x][y] = value;
} int vrat(int x, int y){ return deska[x][y];
}
67
68
Knihovna STL a vybrané nástroje OOP
}; class Sachy{ private: Sachovnice* deska; Hrac* prvni; Hrac* druhy; public: Sachy(string jmeno1, string jmeno2){ deska = new Sachovnice(); prvni = new Hrac(jmeno1); druhy = new Hrac(jmeno2);
} void vypisDesku(){ deska->vypisDesku();
} void nastav(int x, int y, int value){ deska->nastav(x, y, value);
} // Bez toho kodu to bude havarovat Sachy(const Sachy& vzor){ deska = new Sachovnice(); prvni = new Hrac(vzor.prvni->vratJmeno()); druhy = new Hrac(vzor.prvni->vratJmeno()); for(int i=0; i<8; i++) for(int j=0; j<8; j++) deska->nastav(i, j, vzor.deska->vrat(i,j));
} ~Sachy(){ delete(deska); delete(prvni); delete(druhy);
} }; int main (int argc, char * const argv[]) { Sachy* hra = new Sachy("Karl", "Egon"); Sachy*kopie = new Sachy(*hra); hra->nastav(2,2,9); hra->vypisDesku(); kopie->vypisDesku(); delete(hra); delete(kopie); return 0;
}
Grafické knihovny
6
Grafické knihovny
OpenGL, DirectX, XNA a ti další...
Pohádky, pověsti a mýty Na počátku kapitoly, která se má zabývat různými GAPI (grafickými aplikačními prog. roz.) by bylo na místě tyto knihovny srovnat. Paradoxně se jedná o těžký, až neřešitelný úkol. Na toto téma bylo napsáno nespočet článků a na autory se sneslo nespočet kritiky. Důvodů je několik. Jednak se tato GAPI nedají v principu regulérně srovnávat, protože každé má trochu jiný účel, druhý důvod je nízká znalost pisatelů. Autor zpravidla ovládá jedno GAPI, ostatní zná jen ”z rychlíku” a píše srovnání. Třetí a neméně důležitým problémem objektivního srovnání je to, že část těchto knihoven je udržována firmou Microsoft a část je, alespoň morálně, Open Source. Už jen toto je dostatečný důvod k nekonečným diskuzím a ”flamewarům”. Nebudu se snažit přidávat další polínko do ohně diskuzí, zda je lepší ten či onen produkt a pokusím se pragmaticky vyjmenovat některé významné rysy těchto produktů. Srovnání ponechám na laskavém čtenáři.
Kolo první: OpenGL vs. DirectX Nejsilnějšími soupeři v tomto boji jsou OpenGL a DirectX. Popišme si základní rozdíly mezi nimi. • Schopnosti – Jedním z hlavních rozdílů mezi knihovnami je rozsah jejich schopností. OpenGL je knihovna určená pouze pro grafiku, prakticky nejvíce pro 3D grafiku. DirectX je celý soubor knihoven pro práci s grafikou, zvukem, sítí, atp. Jeho smyslem je dát vývojářům her k dispozici kompletní paletu nástojů pro práci s HW. V tomto ohledu DirectX rozhodně vede. Pokud nás ale zajímá hlavně vykreslování a obsluha základního HW, pak žádného přesvědčivého vítěze nemáme. • Podpora programovacích jazyků – DirectX je primárně určeno pro programovací jazyky C++ a C#. Existuje i způsob zpřístupnění DirectX v Javě (hledejte informace o Java 3D). OpenGL je podporováno prakticky všemi běžně používanými jazyky počínaje C/C++ a konče Adou a různými assemblery. • Platformní nezávislost – DirectX je vázáno na platformu různých verzí OS Windows, resp. platformy, na které je implementován .NET framework. V praxi jsou to dnes PC s OS Windows, Xbox konzole a některé mobilní zařízení (zatím spíše teoreticky). OpenGL je podporována prakticky na všech platformách počínaje OS Windows, speciálními UNIXy, GNU/Linuxem a konče herními konzolemi. Toto je, podle mého názoru také největší klad OpenGL. Pokud chcete napsat aplikaci, která musí fungovat i jinde, než pod OS Windows, je OpenGL prakticky jedinou variantou (vědecké vizualizace, virtuální realita, atp.). V poslední době je mnoho diskuzí ohledně implementace nové verze DirectX 10. Tato verze je, k nelibosti uživatelů i vývojářů, k dispozici pouze pod OS Windows Vista, které nemají na trhu tak drtivý podíl, aby zasáhli většinu potenciálních zákazníků (i když se tato situace samozřejmě postupem času vyřeší sama, vývojářům se nelíbí, že jejich nové hry by si mohl spustit pouze zlomek uživatelů všech Windows). • Struktura jazyka – DirectX je od počátku objektově orientované. OpenGL je díky svému historickému původu spíše procedurální, lze jej však samozřejme implementovat do objektových aplikací. • Hardwarová podpora – Hardwarová podpora je základem grafických knihoven. Pokud není knihovna grafickou kartou podporována, je prakticky bezcenná. V tomto ohledu jsou obě zmiňované knihovny vyrovnané. Lze se s úspěchem přít, zda výrobci karet reagují rychleji na nové verze toho či onoho standardu. V případě ”konzumní” kategorie karet je to možná DirectX, v profesionální spíše OpenGL. Diskuze na toto téma je však prakticky bezpředmětná, podstatné je, že všechny běžné grafické karty podporují v určité verzi obě knihovny.
XNA (XNA’s Not Acronymed 25 ) XNA je poměrně nová technologie z dílny firmy Microsoft. Dovolím si tvrdit, že má 2 základní účely: jednak usnadnit tvorbu her a také podpořit tvorbu her pro platformu Xbox. V této verzi je XNA dostupné jako rozšíření pouze pro Visual C# Express, takže není možné
69
70
Grafické knihovny využívat plnohodnotného Visual Studia nebo jiných vývojových nástrojů. Jak plyne z předchozího textu je XNA spojeno s jazykem C# a potažmo .NET frameworkem (technicky lze psát aplikace s podporou XNA v jakémkoliv jazyce, který podporuje .NET, ale prakticky existují nástroje pouze pro C#). Podle informací na stránkách Microsoftu využívá XNA pro grafický výstup DirectX. To je poměrně logické. Jak bylo popsáno v části o HW podpře – pokud grafický jazyk není přímo podporován grafickou kartou, je příliš pomalý pro praktické využití. XNA tedy musí pro HW akceleraci používat buď DirectX nebo OpenGL. Vzhledem k původu standardů byla volba smozřejmě jednoznačná. Jak bylo zmíněno ve větě o základních účelech. Smyslem XNA má být zjednodušení tvorby her. V tomto směru jsou od vyvojářů poměrně kladné reakce. Diskutabilní je ovšem stavba frameworku. Od počátku (přímo dle slov pracovníků Microsoftu) je designován tak, že aplikace využívající XNA bude běžet jako jediná ”náročnější aplikace”. Tedy aplikace s XNA bude využívat maximum HW prostředků pro svůj optimální běh. Nelze tedy předpokládat její využití pro jinou aplikaci, než hru, která v daném okamžiku zabere celý výkon počítače, resp. herní konzole. Tento ne příliš šetrný přístup k optimalizaci aplikací za cenu jednoduchosti je patrný např. i v systému vykreslování. Např. v OpenGL je scéna překreslena pouze tehdy, pokud je ”zneplatněna”, tj. nastane v ní změna (pohyb objektů, posun myši). V případě XNA je scéna cyklicky překreslována maximální možnou rychlostí bez ohledu na (ne)změnu jejího obsahu. Jednou z významných výhod XNA je to, že zjednoduší produkci her pro konzoli Xbox. Hry by mělo jít bez úprav provozovat jak na PC, tak na Xboxu, který obsahuje implemetaci .NET a XNA frameworku.
Open Inventor Zajímavý projektem, který bysme neměli opomenout je OpenInventor. Pro seznámení s touto knihovnou doporučuji seriál Jana Pečivy na serveru Root 26 .
Knihovny Qt, GLUT, aj. Tato skupina knihoven je značně odlišná od těch, o kterých byla řeč doposud. Zatím jsme mluvili o různých knihovnách, které slouží pro tvorbu grafického obsahu aplikací. Při tvorbě pokročilých aplikací však běžně využíváme i druhou skupiny knihoven - pro tvorbu grafického uživatelského rozhraní (tj. okna, tlačítka, atp.). K tomuto účelu existuje celá řada knihoven. Pro nás, jako programátory v C++ je poměrně zajímavá knihovna Qt firmy Trolltech. Je k dispozici jak v OpenSource verzi pro nekomerční účely, tak v placené verzi pro výdělečnou činnost. Je kompletně napsána v C++ a nabízí celou řadu funkcí nad rámec tvorby GUI (síťová komunikace, 2D grafika, ...). V našem kurzu se ještě setkáme s knihovnou GLUT, jedná se o knihovnu úzce spojenou s OpenGL. Běžně ji využívají menší OpenGL aplikace. Lze pomocí ní vytvářet menu, obsluhovat myš, klávesnici, atp.
OpenGL
OpenGL Programování v OpenGL je natolik rozsáhlá problematika, že by si zasloužila zcela samostatný tutoriál. Tato sekce pouze nastiňuje základní myšlenku OpenGL aplikací a rozebírá některé nezbytné funkce. Pro hlubší pochopení celého programu doporučuji např.: • CZ NeHe OpenGL 27 • OpenGL video tutorial 28 • OpenGL evaluátory 29 • Seriál Grafická knihovna OpenGL 30 • Seriál Tvorba přenositelných grafických aplikací využívajících knihovnu GLUT 31 • Seriál OpenGL na serveru Builder.cz 32 26
http://www.root.cz/serialy/open-inventor/ http://nehe.ceske-hry.cz/tut obsah.php 28 http://www.videotutorialsrock.com/ 29 http://www.root.cz/serialy/opengl-evaluatory/ 30 http://www.root.cz/serialy/graficka-knihovna-opengl/ 31 http://www.root.cz/serialy/graficka-knihovna-opengl/ 32 http://www.root.cz/serialy/graficka-knihovna-opengl/ 27
Grafické knihovny V úvodním popisu této knihovny si dovolím citovat kolegu Tišnovského, který ve svém seriálu 33 OpenGL popsal takto: Knihovna OpenGLT M [1] 34 [2] 35 [3] 36 (Open Graphics Library) byla navržena firmou SGI 37 (Silicon Graphics Inc.) jako aplikační programové rozhraní (Application Programming Interface - API) k akcelerovaným grafickým kartám resp. celým grafickým subsystémům. Předchůdcem této knihovny byla programová knihovna IRIS GL (Silicon Graphics IRIS Graphics Library). OpenGL byla navržena s důrazem na to, aby byla použitelná na různých typech grafických akcelerátorů a aby ji bylo možno použít i v případě, že na určité platformě žádný grafický akcelerátor není nainstalován - v tom případě se použije softwarová simulace. V současné době lze knihovnu OpenGL použít na různých verzích unixových systémů (včetně Linuxu a samozřejmě IRIXu), OS/2 a na platformách Microsoft Windows (doplňuji, že i na MacOS X). Z programátorského hlediska se OpenGL chová jako stavový automat. To znamená, že během zadávání příkazů pro vykreslování lze průběžně měnit vlastnosti vykreslovaných primitiv (barva, průhlednost) nebo celé scény (volba způsobu vykreslování, transformace) a toto nastavení zůstane zachováno do té doby, než ho explicitně změníme. Výhoda tohoto přístupu spočívá především v tom, že funkce pro vykreslování mají menší počet parametrů a že jedním příkazem lze globálně změnit způsob vykreslení celé scény, například volbu drátového zobrazení modelu (wireframe model) nebo zobrazení pomocí vyplněných polygonů (filled model). Vykreslování scény se provádí procedurálně - voláním funkcí OpenGL se vykreslí výsledný rastrový obrázek. Výsledkem volání těchto funkcí je rastrový obrázek uložený v tzv. framebufferu, kde je každému pixelu přiřazena barva, hloubka, alfa složka popř. i další atributy. Z framebufferu lze získat pouze barevnou informaci a tu je možné následně zobrazit na obrazovce
Základní struktura OpenGL aplikace Takto vypadá jeden z nejkratších příkladů v OpenGL. Prostě jen vytvoří prázdné okno, do které mohou být vykreslovány další grafické objekty. Základem je nadefinovat několik funkcí, které se budou automaticky v průběhu běhu programu volat. Pravděpodobně nejduležitější je funkce onDisplay. Ta definuje, co se bude dít, při překreslení okna. To této funkce vkládáme veškerý kód, který se týká vykreslování (tvorba grafických objektů, atp.). Druhou funkcí, která stojí za zmínku je onResize. Jak již napovídá její název, určuje, co se bude dít v okamžiku změny velikosti okna aplika. Obvykle přepočítá souřadný systém a zavolá překreslení obsahu okna.
Parametry zaregistrovaných funkcí jsou standardizovány, jejich počet ani typy nelze měnit.
33
http://www.root.cz/clanky/graficka-knihovna-opengl-1/ http://www.root.cz/clanky/graficka-knihovna-opengl-1/#hrf1 http://www.root.cz/clanky/graficka-knihovna-opengl-1/#hrf2 36 http://www.root.cz/clanky/graficka-knihovna-opengl-1/#hrf3 37 http://www.sgi.com/ 34 35
71
72
Grafické knihovny
// pokud pracuje pod MacOS X nebo GNU/Linuxem, tato knihovna se obvykle nepoužívá #include <windows.h> // načteme základní knihovnu pro práci s OpenGL #include // tato funkce se automaticky volá v okamžiku, kdy je změněna velikost okna void onResize(int w, int h) // w a h je nová velikost okna
{ glViewport(0, 0, w, h); // nastavení viditelné oblasti (přes celé okno) glMatrixMode(GL PROJECTION); // příprava projekční matice (prozatím není nutné chápat) glLoadIdentity(); glOrtho(0, w, 0, h, -1, 1); // namapovaní programových souřadnic do souřadnic okna
} // Tato funkce se automaticky volá při každém překreslení okna void onDisplay(void)
{ glClear(GL COLOR BUFFER BIT); // vymazání bufferu glFlush(); // vykreslení změn (zatím není co)
} int main(int argc, char **argv)
{ glutInit(&argc, argv); // inicializace knihovny GLUT glutCreateWindow("Okno"); // vytvoreni okna glutDisplayFunc(onDisplay); // registrace funkce volané při překreslení glutReshapeFunc(onResize); // registrace funkce volané při změně velikosti okna glutMainLoop(); // nekonečná smyčka, kde se volají zaregistrované fce return 0; // sem se program nikdy nedostane
}
Jak je patrné, funkce main bude poměrně stručná. Zpravidla se v ní pouze zaregistrují funkce, které budou obsluhovat určitou činnost (překreslování, změna velikosti, obsluha klávesnice, aj.) a zavolá se pomocí funkce glutMainLoop() nekonečná smyčka, ve které se budou zaregistrované funkce automaticky volat.
Všimněte si, že příkazy použité v programu používají vždy předponu gl nebo glut. Příkazy OpenGL totiž mají svoji pevnou syntaxi a . a
http://www.root.cz/clanky/opengl-2-syntaxe-funkci/
Obsluha klávesnice a myši Pokud chcete v OpenGL aplikaci používat také klávesnici a myš, stačí zaregistrovat následující funkce:
Grafické knihovny
void onKeyboard(unsigned char key, int x, int y)
{ if (key==’q’) exit(0); // pokud někdo zmáčkne klávesu q, skonči ...
} // parametry říkaji, jaké tlačítko bylo zmáčnuto, // jestli bylo zmáčknuto nebo puštěno a jaká byla pozice myši void onMouse(int button, int state, int x, int y)
{ switch(button) { case GLUT LEFT BUTTON: // pokud někdo zmáčně levé tlačítko myši ... break; case GLUT RIGHT BUTTON: // pravé tl. ... break; case GLUT MIDDLE BUTTON:// prostřední tl. ... break;
} glutPostRedisplay(); // zavolej překreslení okna
} ... glutKeyboardFunc(onKeyboard); // registrace fce volané při stisku klávesy glutMouseFunc(onMouse); // registrace fce volané při stisku tlačitka myši
Vykreslování grafického obsahu V OpenGL můžeme vykreslovat z hlediska projekce buď 2D nebo 3D a z hlediska způsobu vykreslování buď grafická primitiva (kvádr, jehlan, koule, ...) nebo vrcholy. Tyto vrcholy pak mohou být spojovány do různých útvarů 38 (trojúhelníky, čtyřúhelníky, ...). Ukažme si nejdříve práci s vrcholy ve 2D. Jako elementární příklad nám může posloužit vykreslení barevného trojúhelníku. Oproti základnímu příkladu nám postačí modifikovat funkci onDisplay následujícím způsobem:
void onDisplay(void)
{ glClear(GL COLOR BUFFER BIT); glBegin(GL TRIANGLES); // začni vytvářet trojúhelníky glColor3i(255, 0, 0); // určím rgb barvu prvního vrcholu glVertex2i(50, 0); // poloha prvniho vrcholu glColor3i(0, 255, 0); // barva druhého vrcholu glVertex2i(0, 100); // poloha druhého vrcholu glVertex2i(100, 100); // poloha třetího vrcholu glEnd(); glFlush(); // vykreslení změn
}
Výše uvedený příklad krásně ilustruje základní vlastnost OpenGL - stavovost. Na počátku funkce příkazem glBegin(GL TRIANGLES) určím, že od tohoto okamžiku se ze zadaných vr38
http://www.root.cz/clanky/opengl-3-zakladni-geometricke-prvky/
73
74
Grafické knihovny cholů budou tvořit trojúhelníky. Pokud zadám 3 vrcholy, vytvoří se jeden trojúhelník, pokud jich zadám 12, vykreslí se 4. Další příklad stavovosti je práce s barvami vrcholů. Definoval jsem si barvu vrcholu pomocí glColor3i před prvním a druhým vrcholem. Těmto vrcholů byly přiřazeny příslušné barvy. Třetímu vrcholu jsem žádnou explicitní barvu nepřiřadil, proto bude mít přiřazenu barvu naposledny řečenou - tu pro druhý vrchol. (Kompletní příklad naleznete v řešených příkladech k procvičení). Jak již bylo řečeno, problematika OpenGL je velmi komplexní a dala by se na ni napsat řada tutoriálů. Lze však s jistotou tvrdit, že již těchto několik kusých informací, které zde byly uvedeny Vám bohatě stačí k tomu, abyste si v OpenGL zkusili napsat vlastní aplikaci. Zkuste třeba následující:
Vytvořte jednoduchou hru ”Tanky”. Na obrazovku vykreslete 2 čtverce různých barev jako dva tanky a ovládejte je pomocí klávesnice. (Při klepnutí na klávesu se zvětší nebo zmenší pomocná proměnná, která bude reprezentovat změnu pozice tanku v ose x nebo y. Tuto proměnnou pak přičítejte k pozici vrcholu tanku)
Příklady Příklad 1 Vytvořte aplikaci v OpenGL vykreslující trojúhelník s různě barevnými vrcholy. Aplikaci bude možné zobrazit na celou obrazovku a ukončit pomocí klávesnice. Barvy vrcholů trojúhelníka by mělo jit zapínat a vypínat pomocí tlačítek myši. Řešení:
#include <windows.h> #include int width=0; int height=0; float red=0.3; float green=0.3; float blue=0.3; void onResize(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL PROJECTION); glLoadIdentity(); glOrtho(0, w, 0, h, -1, 1); width=w; // zapamatovani nove velikosti okna height=h;
} void onDisplay(void) { glClear(GL COLOR BUFFER BIT); glBegin(GL TRIANGLES); glColor3f(0.3, 0.3, blue); glVertex2i(width>>1, 0); glColor3f(0.3, green, 0.3); glVertex2i(width, height); glColor3f(red, 0.3, 0.3); glVertex2i(0, height); glEnd(); glFlush(); /* vykresleni zmen */
} void onKeyboard(unsigned char key, int x, int y){ switch (key) { case 27: // ukonceni
Grafické knihovny
exit(0); break; case ’f’: // fulscreen glutFullScreen(); break; case ’w’: // zmenseni do okna glutReshapeWindow(300, 300); // zmena velikosti okna glutPositionWindow(100, 100); // posun okna break; default: break;
} } void onMouse(int button, int state, int x, int y) { switch(button) { /* ktere tlacitko je stlaceno? */ case GLUT LEFT BUTTON: red=(state==GLUT UP)?0.3:1.0; // vzpominate na ternalni operator? :-) break; case GLUT RIGHT BUTTON: green=(state==GLUT UP)?0.3:1.0; break; case GLUT MIDDLE BUTTON: blue=(state==GLUT UP)?0.3:1.0; break;
} glutPostRedisplay();
} int main (int argc, char **argv) { glutInit(&argc,argv); glutInitDisplayMode(GLUT RGBA|GLUT SINGLE);// graficky mod glutInitWindowSize(800, 600); glutInitWindowPosition(100, 100); glutCreateWindow("Moje okno"); glutDisplayFunc(onDisplay); glutReshapeFunc(onResize); glutKeyboardFunc(onKeyboard); glutMouseFunc(onMouse); glutMainLoop(); return 0;
}
Závěr Tento eLearningový kurz se Vám pokusil nabídnou ucelený pohled na základy objektově orientovaného programování v C++. Na Internetu naleznete celou řadu kurzů na objektové programování nebo na programování v C++. Málo kdy ale autoři C++ tutoriálů dbají na propojení těchto dvou úzce spolu souvisejících věcí. Tento tutoriál se o to, snad úspěšně, pokouší. Provedl Vás základy objektové ho návrhu a teprve tehdy, až jste získali základní znalosti pro tvorbu aplikací, začal Vám přestavovat složitější konstrukce jazyka. Při demonstračních příkladech jsme se snažili dbát na to, abychom nezahodili učivo první části (OON) a nesoustředili se pouze na syntaxi jazyka, ale ukázali Vám problém v jeho reálné, objektové, povaze. Doufáme, že stejným způsobem budete programovat i Vy. Objektový návrh není jen filosofie nebo teoretická nepraktická metodika, je to velmi propracovaný způsob myšlení při tvorbě programů. Je trochu obtížné do něj proniknout, ale pokud se Vám to podaří, odměnou Vám bude zcela nový pohled na tvorbu programů jehož benefit pocítíte hlavně při tvorbě složitějších aplikací.
75
76
Slovníček pojmů
Slovníček pojmů Abort: Funkce C++, která přiřadí programu návratovou hodnotu 3 a ukončí ho. Abstraktní třída: Abstraktní třída (abstract class) je třída, která obsahuje alespoň jednu virtuální metodu. Není možné vytvářet instance abstraktní třídy. Smyslem abstraktní třídy je poskytovat jednotné rozhraní pro přístup ke svým potomkům (všichni potomci musí obsahovat implementaci čistě virtuální metody). Agreagace: Agregace (aggregation) je forma vazby, která vyjadřuje skutečnost, že na jeden objekt (agregovaný objekt, část) je udržován odkaz jiným objektem (agregátem, celek). Agregovaný objekt vzniká a zaniká zcela nezávisle na celku. Pouze je na něj po určitou dobu běhu programu udržován odkaz. Argument: Argument (argument) je parametr funkce, tedy hodnota, která se funkci předává při jejím volání. Asociace: Asociace (association) je nejvolnější vazba mezi třídami (objekty), instance tříd spojených asociací si mohou zasílat zprávy (volat svoje metody). Někdy je pojem asociace používán ve smyslu obecná, neupřesněná, vazba. Atribut: Atribut (attribute) je vlastnost (charakteristika) objektu (je to proměnná navázaná k objektu). Atributy slouží k ukládání dat do jednotlivých objektů. Třída obsahuje předpis, který říká jaké vlastnosti a jakých typů se budou zaznamenávat. Atribut objektu obsahuje konkrétní hodnotu vlastnosti (barva auta je červená). Const: Z klasické promněnné const vytváří konstantu. U metody označuje, že metoda nemění vnitřní stav objektu. U objektu pak, že objekt nemění svůj stav (atributy). Čistě virtuální metoda: Čistě virtuální metoda (pure virtual method) je virtuální metoda, která nemá žádnou implementaci. Nelze ji zavolat a slouží tedy jen pro definici společného rozhraní všech potomků (viz abstraktní třída). Členská proměnná: V C++ můžeme definovat i atributy, které náleží přímo třídám, označujeme je zpravidla jako členské proměnné. Hodnota členské proměnné je sdílena mezi všemi instancemi dané třídy. Destruktor: Destruktor (destructor) je metoda automaticky volaná při rušení objektu (příkaz delete nebo konec programu). Funkce: Funkce je nástroj, který umožňuje vyjmout část kódu z programu a ten podle potřeby opakovaně volat. Typickým příkladem je složitý výpočet, který opakovaně voláme na různých místech programu. Funkce, která náleží k objektu (třídě) je označována jako metoda. Globální proměnná: Tento typ proměnné lze číst a modifikovat z jakéhokoliv místa programu. Implementace: Implementace (implementation) je uskutečnění, realizace dané myšlenky, postupu, modelu atp. V programování se tedy zpravidla jedná o vytvoření programu (resp. zdrojového kódu) s daným chováním - tj. napíši tělo metody, funkce. O implementaci mluvíme v souvislosti s deklarací. Implicitní: Implicitní (default) znamená výchozí, skrytý, automatický. Například výchozí hodnota nastavení programu je hodnota definovaná z výroby (tj. ve zdrojovém kódu), tato hodnota je uživateli skryta, ale při každé instalaci programu se automaticky nastaví. Inicializace: Inicializace (initialization) je soubor operací jejichž výsledkem je nastavní počátečních hodnot proměnných. Inline: Inline je klíčové slovo, které říká překladači, že metoda (funkce) je velmi krátká a že má místo jejího volání dosadit při překladu rovnou její kód. Ušetří se tak čas za její volání při mírném zvětšení výstupního kódu. Překladač toto návěstí může ignorovat. Instance třídy: Instance třídy je objekt. Pokud budeme mluvit neformálně, lze říci, že třída je předpis, jak má určitý objekt vypadat (bude to auto a má mít barvu, velikost) a objekt je vyplnění této ”šablony” (červená škoda 105 délky 4m). Kompozice: Kompozice (composition) je silnější forma agregace, ve které jsou oba objekty spojeny tak úzce, že jeden bez druhého nemůže existovat - pokud vznikne celek, vzniknou i jeho části a pokud celek zanikne, zaniknou i části. Konstruktor: Konstruktor (constructor) je metoda automaticky volaná při vytváření objektů. Rozlišujeme parametrický a bezparametrický konstruktor. Speciálním typem konstruktoru je implicitní prázdný konstruktor. Není uveden v kódu a je automaticky zavolán, pokud není ručně nadefinován jiný konstruktor. Nedělá nic. Kopírovací konstruktor: Speciální typ konstruktoru, který se stará o vytvoření kopie objektu. Kopie může být plytká (kopírují se jen hodnoty atributů, tj. i ukazatelů) nebo hluboká (kopírují se i struktury v paměti na které ukazují ukazatele). Implicitní kop. konst. vytváří vždy pouze plytkou kopii.
Slovníček pojmů Lokální proměnná: Lokální proměnná platí pouze v dané funkci nebo struktuře. Případně bývá deklarována v rámci cyklu, pak je její platnost omezena na tento cyklus. Metoda: Metoda (method) je funkce přiřazená objektu, tedy funkce definovaná ve třídě. Modifikátor viditelnosti: Modifikátor viditelnosti je klíčové slovo private, protected nebo public. Pomocí modifikátorů viditelnosti lze skrývat atributy a metody třídy, ke kterým není vhodné přistupovat mimo tuto třídu. Objekt: Objekt (object) je instancí třídy, je to konkrétní část modelu skutečnosti. Charakteristiky a funkce objektu určuje jeho třída a objekt pouze určuje konkrétní hodnoty svých atributů (vlastností). Více viz ”instance třídy”. Parametr: Parametr (parameter) je argument funkce, tedy hodnota, která se funkci předává při jejím volání. Procedura: V Pascalu a jiných jazycích se můžeme setkat s pojmem procedura. Označuje se tak funkce, která nevrací žádnou hodnotu (jen provede např. výpis na obrazovku nebo zmodifikuje určitou globální proměnnou). V C++ je procedura nahrazena funkcí vracející void. Překrytí: Překrytí je často zaměňováno s přetěžováním. Překrytá metoda je taková, která se vyskytuje jak u předka, tak u potomka a má úplně stejnou hlavičku a rozdílnou implementaci. Přetěžování: Přetěžování (overloading) je postup při kterém je definováno více metod stejného jména, které se liší počtem a/nebo typem argumentů. Při zavolání metody pak překladač rozhodne, kterou z metod použije podle předaných parametrů. Rozhraní: O rozhraní (interface) lze mluvit ve více souvislostech. Může to být skupina metod dané třídy, které jsou veřejné. Ty zajišťují rozhraní pro manipulaci s objektem. Interface je také speciální konstrukce v Javě, ketrá umožňuje přístup pouze k určitým metodám dané třídy. Obecně je to také část programu (objekt/třída, funkce, programový modul), která umožňuje stejný přístup k různým programovým částem (objektům/třídám, funkcím, modulům). Rozhraní tedy spojuje několik vzájemně rozdílných částí programu nebo i celých programů. Například třída, která funguje jako rozhraní mezi třídami, obsahuje všechny společné prvky obou tříd. Rozhraní je například možné vyrobit využitím dědičnosti, kdy rodičovská třída tvoří rozhraní (poskytuje jednotný přístup) ke svým potomkům (čistě virtuální metoda). Řetězec: Řetězec (string) je datový typ pro ukládání textu. Static: Z klasického atributu nebo metody udělá členskou proměnnou nebo členskou metodu (funkci). Je-li static globální proměnná (nebo funkce), je viditelná pouze v daném souboru zdrojového textu (modulu). Je-li static lokální proměnná funkce, potom její hodnota je uchovávána mezi jednotlivým voláním (zaniká při ukončení programu) Struct: Je v C++ obdoba class. Jedná se o prakticky totožnou konstrukci. Liší se v tom, že struct má implicitně metody a atributy public (class je má private). Terminate: Funkce, která je zavolána v případě, že některá výjimka není vůbec ošetřena. Tj. nebyl nalezen příslušný blok catch do konce hlavní funkce programu. Pokud není řečeno jinak, volá funkci abort. This: this je speciální proměnná, ve které je odkaz na aktuální objekt, tato proměnná je k dispozici pouze uvnitř metod. Jedná se o implicitní parametr, který není uváděn (narozdíl např.od Pythonu). Přes this přistupujeme např. k atributům třídy, pokud mají stejné názvy jako parametry metody nebo lokální porměnné. Třída: Třída (class) je abstrakní vzor objektů, který definuje charakteristiky objektu (atributy) a funkce všech objektů příslušných k této třídě. Lidově řečeno se jedná o ”šablonu”, jak mají být objekty dané třídy charakterizovány. Unexpected: Funkce, která je zavolána v případě vyhození neočekávané výjimky. Pokud není řečeno jinak, zavolá funkci terminate. Její chování modifikovat. Vector: Jedná o datový kontejner knihovny STL. Jedná se o šablonu dynamického pole. Od vectorujsou odvozeny další abstraktní datové struktury (zásobník, fronta, ...). Virtuální metoda: Virtuální metoda (virtual method) je metoda, jejíž implementace se liší mezi rodičovskou třídou a jejímy potomky (odvozenými třídami). Pokud do ukazatele na předka dosadím potomka, pak při zavolání virtuální metody u objektu typu rodičovské třídy dojde ve skutečnosti k zavolání metody u skutečné třídy (dosazeného potomka) objektu (viz část polymorfismus). Příklad definice a volání virtuální metody v C++: class RodicovskaTrida { virtual metoda(); }; class Potomek {
77
78
Slovníček pojmů
metoda();
};RodicovskaTrida *objekt = new Potomek(); objekt->metoda(); Výjimka: Výjimky jsou mechanismus, kterým se snažíme zabránit pádu programu v důsledku provedení určité neplatné operace (zadání špatné hodnoty, přerušení TCP spojení, atp.). V principu se jedná o provedení kódu ”na nečisto”. Pokud je provedení korektní, pokračuje se dál. V opačném případě je vyhozena výjimka. Nevýhodou výjimek je, že spotřebovávají větší množství systémových prostředků, než klasické podmínky. Zapouzdření: Zapouzdření (encapsulation) je operace při které jsou skryty detaily implementace objektu a veřeně přístupné zůstavají jen vybrané metody a atributy, se kterými má mít uživatel právo manipulovat. V principu se snažíme aby maximum implementace zůstalo skryto. Používáme k tomu modifikátory viditelnosti. Zasílání zpráv: Zasílání zpráv je proces, který zajišťuje spuštění příslušné metody příslušného objektu. Tedy metoda jedné třídy volá metodu jiné třídy. Název volané metody je označován jako zpráva.