Tvorba třídy 2D pole a tříd odvozených



Úkol

Vymyslete alespoň dvě aplikace, jejichž součástí je dvourozměrné dynamické pole (maticový počet pro reálná čísla, komplexní čísla ...). Vytvořte třídy pro práci s těmito prvky.



Budeme postupovat podle postupu Návrh třídy.

Příklady využívají zdrojových textů (nejčastěji definice proměnných) z minulých bodů.



Krok 1. Co bude třída dělat

Jedná se o rozbor úlohy, návaznost na jazyk je zatím minimální.

Co by měla třída (pro počáteční představu spíše knihovna, „výpočetní modul“, ...) pro počítání s maticemi umět? Vybavte si v jakých situacích jste matice viděli, k čemu se používají. Dá se tato třída „rozdělit“ (například používá nějaký celek, který od ní lze oddělit a použít jinde)?

Pro matici je základem 2D pole nesoucí data. Třídu 2D pole určitě využijeme i samostatně (tj. například pro obdélníkové rastry bez použití inverze, násobení, ...). Úvahy proto rozdělíme na obecné vlastnosti 2D pole a na nástavbové vlastnosti maticových výpočtů.



Krok 2. Jak bude třída pracovat?

Opět „bez návaznosti“ na jazyk promyslet v jakých situacích budeme matice využívat. Jelikož už jsme si řekli, že součástí matice je 2D pole, tvoříme vlastně dvě třídy a úvahy provádíme pro obě.
Promyslet si jak matice budou vznikat (kde se zjistí/nastaví rozměr matice, jakou hodnotou se při vzniku naplní ...). Promyslíme si co se stane, má-li vzniknout matice a nejsou dodány rozměry a/nebo hodnota v matici. Zda umožníme vznik/vytvoření matice na základě jediného „čísla“ a co bude znamenat (například čtvercová matice naplněná nulami, nebo matice 1x1 naplněná zadanou hodnotou ...). Užitečné bude vytvoření matice ze dvou nebo tří hodnot (rozměry a hodnota). A dále potom vytvoření na základě jiné matice. Vytvoření na základě dat v souboru ...

Dále si promyslet manipulaci s prvky matice - jak je budeme číst, zda umožníme uživateli změnu prvků a zda existují i „chybné“ hodnoty a jak vyřešit jejich zadání. Zda existuje „nepoužitelná“ matice (jako důsledek neinicializace, chyby při výpočtu, ...) a jak budeme tento stav signalizovat. Každý výpočet by měl testovat, zda-li jeho vstupem jsou plně inicializované matice. Před ukončením by se objekt měl nastavit do konzistentního datového stavu, vrátit chybový kód.

Zda budou operátory: zda je možné přiřadit hodnotu jedné matice druhé, logické porovnávání (problém kdy je matice větší? u stejné to není problém), sčítání, násobení ....

Budou nějaké jiné metody? Například inverze, transpozice (standardně pro transpozici není vyhrazen v jazyce C/C++ žádný operátor. Bylo by možné předefinovat některý z existujících operátorů (-,!,~). Je ovšem na zvážení zda z takto zapsané operace bude patrné, že se jedná o transpozici. Platí pravidlo očekávaného chování a proto zde by bylo vhodnější definovat metodu Transponuj).

Vstup a výstup - jak je budeme ukládat a prezentovat (disk, obrazovka - serializace dat z/do instance).

Dědění: 2D pole je určitě knihovna, která je využitelná nejen pro matice, ale obecně pro různé jiné aplikace. Je proto výhodné vytvořit třídu 2D pole a tuto ve třídě matice využít.
Otázkou je, jestli třída matice z 2D pole dědí, nebo ho vlastní (patří mezi její členská data). Jedná se o zhodnocení, zda se jedná mezi třídami o vztah „je“ nebo „má“.
Matice “je“ (rozšíření) 2D pole, protože by měla umět vše co 2D pole (transpozice, změna rozměrů, přidání řádků a sloupců ...). Kromě toho si matice přidá specializaci (operátory - sčítání, násobení, inverze ...). V této variantě používáme (jako uživatel) pro matici přímo metody 2D pole. Pokud mají dvě třídy vztah „je“, potom používáme dědění.
Pokud je 2Dpole pouze (jednou z mnoha) součástí/prvkem/proměnnou, nebo potřebujeme aby nový objekt používal několik (rovnocenných) polí, hodí se lépe vztah má - to znamená, že nedědíme, ale nová třída obsahuje/vlastní jednu nebo několik instancí původní. Vlastnosti původního prvku nepoužívá uživatel přímo, ale zprostředkovaně přes novou třídu (např. obrázek používá/má/obsahuje čtyři pole pro složky RGBA).

Řešení třídy matice tedy rozdělíme na 2D pole a z ní zděděnou matici.

V této části se také rozhodneme, zda bázová třída (2Dpole) bude využívána samostatně - v případě že ne, zvolíme ji jako ryze virtuální (to je takovou, aby nešel vytvořit její prvek), nebo využijeme jiný mechanizmus k zabránění jejího vytvoření. Virtuální funkce využíváme i v případě požadavku jednotného interface - když budeme pracovat současně s různými odvozenými třídami.



Krok 3. Datová reprezentace

Jelikož chceme dělat matice, nabízí se typ double, který je „univerzální“ pro běžnou práci s maticemi. Otázkou ovšem je, zda typ 2Dpole je vhodné koncipovat (pouze) pro double. Určitě budeme potřebovat i matice pro int, ... nebo složitější typy (CKomplex, CColor, ...). Bylo by tedy výhodné 2DPole ( a následně i matici) koncipovat pro obecný typ. K tomu slouží šablony (template) ale není výhodné s tímto typem řešení začínat (nejdříve si zkusíme vytvořit „klasickou“ třídu, kterou následně přepíšeme do šablon (z výukových důvodů, zkušenější programátoři mohou postupovat přímo)).
Začněme tvořit třídu pro určitý typ. Abychom nemuseli později hledat, které proměnné souvisí s typem v poli a které mají stejný typ samy o sobě (například typ int u rozměru pole nesouvisí s typem int pokud máme pole intů), označme typ pro proměnnou pole pomocí define, např. #define MAT_TYP double. Po odladění je možné zkusit verzi se šablonami (kde právě MAT_TYP bude nahrazen typem šablony).

Co se členských datových prvků 2Dpole týká, bude určitě potřebné mít ukazatel na ukazatel pro vlastní dat pole, rozměry pole, signalizaci platnosti dat (předchozí chyby), ... V odvozené matici není nutné datové prvky přidávat, pokud stačí prvky bázové třídy. Pokud by to bulo nutné, je samozřejmě přdání dalších atributů možné.



Krok 4.

Postup programování je libovolný - zda začneme bázovou třídou a následně uděláme zděděnou, nebo zda budeme třídy dělat souběžně. Každopádně je nutné udržovat projekt v „přeložitelném“ a nejlépe funkčním stavu. („Malé“ kroky se lépe ladí).

Nejlepší přístup je, promyslet si strukturu programu, rozdělení do tříd a jejich vazby. Můžete použít například UML diagram.



Krok 5. Vytvoření (souborů) projektu

Pro projekt vytvoříme základní soubory. Každá třída by měla mít svůj hlavičkový a zdrojový soubor. Dále by zde měl být soubor, který bude obsahovat main, a ve kterém bude kód určený k testování vlastností dané třídy. Hlavičkové soubory budou ošetřeny proti vícenásobnému načtení a naincludovány do příslušných hlavičkových a zdrojových souborů (se kterými) souvisí.

Vytvoříme soubory pro všechny třídy včetně zděděných. Zvážíme použití jmenných prostorů (lze zavést i později).

Vytvoříme základní definici třídy class XXX {};. Od této chvíle již můžeme název třídy používat, a můžeme tedy zkusit definici objektu v main

// ========== hlavičkový soubor třídy =============

#ifndef JEDINECNY_NAZEV

#define JEDINECNY_NAZEV

// tady bude příprava – include, define, ...

class CPole2D{

// tady bude vše důležité o třídě

}

// tady budou těla inline metod

// prototypy pro pomocné funkce související se třídou

#endif

// ======================== testovací soubor s main

int main()
{
{ // blok testovani trid
CPole2D a;
//CMatice b;
} // zde končí blok testovani tříd a „zivot“ vsech promennych
return 1;
}




Krok 6. Datové členy

Ve třídě zavedeme datové členy (vždy preferujeme umístění do privátní sekce).
Pro datové členy zvolíme vhodný typ (rozměry int, pole MAT_TYP **).

Zavedeme statické datové členy a statické metody třídy. Statické členy budou počítat vzniklé a aktivní prvky. Statické metody vrátí počet aktuálně „živých“ a počet vzniklých prvků. Tyto mechanizmy pro počítání nejsou obecně nutné, my je však implementujeme z výukových důvodů. Protože statický člen existuje i bez přítomnosti objektu třídy, musíme ho nadefinovat ve zdrojovém souboru třídy. Stejně tak i statické metody.

static int Pocet; // deklarace v těle třídy
static int Living(void); // deklarace v děle třídy

int CPole2D::Pocet = 0; // definice s inicializací ve zdrojovém souboru (patřícímu ke třídě)
int CPole2D::Living(void){return Pocet;} // definice statické metody ve zdrojovém souboru

Nyní se můžeme zeptat na počet aktuálních prvků a počet prvků, které vznikly za činnosti programu. (zde bude nutná „spolupráce“ konstruktorů (přičtou jedničku) a destruktoru (odečte od „živých“ jedničku)). Do main můžeme vložit následující kód (nebo si napsat funkci). Každopádně ho dáme za konec bloku testování (před poslední návrat z main), kde by neměla existovat již žádná proměnná. Toto provedeme pro každou třídu.

cout << “ pocet zivych prvku “ << CPole2D::Living() << “ Pocet celkove vzniklych “ << CPole2D::Total() << endl;

k tomu bude nutné přidat knihovnu pro vstup/výstup a „zveřejnit“ do globálního jmenného prostoru následující

#include <iostream>

using std::endl;
using std::cout;



Krok 7 Psaní metod

Před psaním bychom si měli promyslet/navrhnout rozhraní/prototypy pro metody třídy.

Metodami jsou:
- konstruktory a destruktory, kterými inicializujeme a „uklízíme“ datové složky/atributy objektu (instance třídy)
- settery a gettery, kterými nastavujeme proměnné a zjišťujeme hodnoty proměnných
- metody pro práci se třídou
- operátory - je možné napsat jakýkoli operátor, který existuje pro standardní typy (pokud operátor dokážeme napsat s proměnnými int, měl by být použitelný i pro naši třídu).
- metody vstupu a výstupu, kterými „tiskneme“ a „načítáme“ hodnoty třídy

U metod rozlišujeme, zda jsou
- privátní - pouze pro použití třídy
- veřejné - tvoří interface a jsou prostředníkem mezi třídou a uživatelem

Dále si u metod rozhodneme zda budou:
- inline - kratší metody, pro které se při překladu nevytváří v kódu funkční volání, ale „rozbalí“ se do kódu (je to vlastně jen předpis pro vytvoření kódu, obdobně jako funkční makra s parametry v C). Jejich kód je napsán v hlavičkovém souboru, přímo uvnitř v definici třídy, nebo jsou jejich hlavičky v definici třídy označeny klíčovým slovem inline (a tělo následuje v h souboru za definicí třídy).
- metody s funkčním voláním - v definici třídy nemají tělíčka, která jsou ve zdrojové části společně s prototypem metody, který je rozšířen o příslušnost ke třídě Trida::metoda.

Parametry metod, užití const
- pro předávání proměnných tříd a struktur do metod (funkcí) používáme přednostně reference, v odůvodněných případech ukazatel, výjimečně hodnotu.
- pokud se argument metody v jejím těle nemodifikuje, označíme (si) ji jako const (toto není povinné, ale rozumné - z hlavičky vidíme, které proměnné se mění a které ne)
- metody, které nemění volající objekt (this) označíme jako konstantní (na konstantní objekty/instance můžeme volat pouze konstantní metody. Nezapomínejte, že TYP a const TYP, jsou dva různé typy a lze proto vedle sebe mít metody pro TYP a const TYP.)
- pro návratové hodnoty používáme referenci v případě, že vracíme proměnné, které existuje i vně metody (funkce). Totéž platí i pro ukazatel. Pokud nelze jinak vracíme hodnotu (pro návratový mechanizmus musí třída obsahovat kopykonstruktor).

- u metod musíme též uvažovat jak se má metoda chovat. Představme si metodu Transpozice. Jak má fungovat, pokud si ji prvek zavolá? Má transponovat aktuální prvek, nebo má aktuální prvek nechat a vytvořit prvek nový, transponovaný (který vrátí)?

Friend metody a operátory
- pokud není možné chování realizovat pomocí členské metody (tj. ne přímo ve třídě, ale musí se realizovat jako funkce) a potřebujeme rychlý/přímý přístup ke členským datům, můžeme využít vlastnosti friend a funkci umožnit přístup k priváte složkám/atributům. Používání friend bychom si měli rozmyslet a neměli bychom ho používat zbytečně, neboť jeho nadměrným používáním knarušujeme koncept zapouzdření objektu.



Krok 8 Konstruktory a destruktor

Konstruktory slouží k inicializaci členských dat těsně po vzniku objektu, destruktor potom k „uklizení“ objektu před zánikem.

Konstruktory nelze běžnými způsoby volat přímo (tj. jako ostatní metody), ale pouze zprostředkovaně zápisem v definici proměnných.

Můžeme například napsat část kódu (za začátek bloku testování). Při implementaci nezapomínejte na počítání objektů pomocí statických proměnných:

CPole2D pa; // implicitní konstruktor - vytvoří pole aniž by k tomu byly jakékoli hodnoty (jak? Tak jak jsme si to nadefinovali.)
CPole2D pb(5), pc(6.3),pd(“<2,2>,{{12,13}{45,7}}“); // konverzní konstruktory - vytvoří pole z jedné hodnoty (int, double, řetězce)
CPole2D pe(pd); // kopykonstruktor - jelikož se vytváří proměnná ze stejného typu, vytváří se kopie
CPole2D pf(2,3,10); // pole ze tří parametrů, první dva jsou rozměr, třetí hodnota, kterou se naplní
int Pole[10] = {0}; CPole pg(2,5,Pole); // ze tří parametrů - rozměr a data v lineárním poli
CPole2D ph[5]; //na pole je volán implicitní konstruktor pro každý objekt pole

Definice konstruktorů (a destruktoru) vypadá v těle třídy například takto (v hlavičkovém souboru)

CPole2D(void) { } //implicitni konstruktor realizován jako inline – má tělo v těle třídy
inline CPole2D (int aRozmer) , //konverzni konstruktor – realizován jako inline díky klíčovému slovu
CPole2D (CPole2D const & aOrig); // kopykonstruktor – realizován pomocí funkčního volání
~CPole2D (void) { } // destruktor

tělo konverzního konstruktoru (je inline) bude v hlavičce umístěné za definicí třídy

CPole2D::CPole2D (int aRozmer) { } // jelikož už nejsme uvnitř prostoru třídy, musíme použít operátor příslušnosti

tělo kopikonstruktoru je ve zdrojové části a vytváří metodu, která se bude volat pomocí funkčního volání (a ne vkládat do kódu jako u inline). Musí předcházet include hlavičkového souboru.

CPole2D::CPole2D (CPole2D const& aOrig) { } // tělo, které určuje činnost konstruktoru

Destruktor je volán opět automaticky překladačem, v okamžiku kdy objekty zanikají (je možné ho volat i jako metodu ale nedělá se to). Proměnné zanikají na konci bloku, ve kterém byly definovány. Destruktory jsou volány automaticky v opačném pořadí jako byly volány konstruktory.

Jelikož každý objekt naší třídy CPole2D obsahuje dynamická data (alokované 2D pole), musí destruktor zajistit odalokování, jinak se přidělená paměť „ztratí“, protože už ji nebude jak odalokovat (s objektem zanikne i informace o tom kde tato paměť leží) a zůstane obsazená. Tato situace se nazává Memory Leak. S tím souvisí i problém tzv. „mělké kopie“ dat, kdy dva prvky ukazují na stejná data (v našem případě dynamické 2D pole) a jelikož destruktor toto neumí zjistit, provede odalokaci dat a ostatní prvky tím pádem „míří“ na nenaalokovanou paměť. K této chybě nejčastěji dochází u kopykonstruktoru a následně u operátoru přiřazní tj. operátoru =. Proto je nutné vytvořit pro každý prvek vlastní dynamická data (existují i metody, kdy se počítají na alokovanou paměť odkazy a odalokuje ji až poslední, ale to je pro nás zbytečně složité).

Protože manipulace s pamětí (změna rozměrů, vznik, zánik ...) bude poměrně častá, vyplatí se pro alokaci a odalokaci vlastního pole v objektu mít pro tuto činnost metody. Tyto metody by neměly být uživatelem volány přímo a proto by měly být v sekci private. Ostatní uživatelovi dostupné metody (konstruktory, destruktor, metody jako Set, ReSize ...) je budou používat ke své činnosti.

Objekty lze vytvořit v paměti i jako dynamické proměnné. Pro vznik/alokaci je nutné použít new, který zajistí alokaci proměnné a zavolání konstruktoru (malloc nezavolá konstruktor a proto ho nemůžeme použít). Pro odalokaci je nutné použít delete (ne free, které nezavolá destruktory), u prvků alokovaných new se odalokace neprovádí automaticky. Je nutné rozlišit mezi odalokací jednoho objektu a pole.

CPole2D *pj = nullptr, *pk = nullptr;
pj = new CPole2D(2,4,0); // alokace objektu pole s rozměry 2x4 a jeho naplnění nulami
pk = new CPole[3]; // alokace tří objektů vzniklých implicitně

if (pj) delete pj; // odalokace jednoho prvku (dynamické proměnné můžeme odalokovávat v libovolném pořadí) - volá destruktor
if (pk) delete[ ] pk; // odalokace pole - volá destruktor pro každý prvek



Krok 9 zadávání a čtení dat

U pole můžeme měnit jeho rozměr (zde je potom nutné promyslet jak se budou transformovat data do nového rozměru, čím se doplní prvky při zvětšení ...), nebo hodnoty (zde je nutné promyslet, zda je dobré tuto vlastnost povolit, popřípadě, zda je nutné vkládaná data kontrolovat). Při každé operaci je nutné kontrolovat zda zadávané rozměry/pozice jsou smysluplné (rozměry nezáporné, pozice není mimo pole ...). Chybnou operaci musíme vyřešit, například ji neprovést, „opravit“ hodnotu podle logiky věci, zahlásit chybu například pomocí tzv. výjimky – nástroj pro řešení chyb v objektovém C++.

CPole2D pa,pb,pc, *pj=&pa;
pa.Resize(5,3,10); // změna rozměru s hodnotou pro doplnění
pb.AddRow(); // přidání řádku
pj->AddColumn(8); //přidání sloupce se zadáním doplňované hodnoty
pc.Resize(-3,0); // špatné hodnoty rozměrů

Pro zadávání a čtení hodnot můžeme použít metody hlavičky ve třídě

double Get(int,int); // získání hodnoty na pozici
void Set(int,int,double); // nastavení hodnoty na pozici
double Change(int,int, double); // nastavení hodnoty na pozici a získání hodnoty původní

implementace ve zdrojovém textu

double CPole2D::Get(int,int) {} // získání hodnoty na pozici
void CPole2D::Set(int,int,double) { } // nastavení hodnoty na pozici
double CPole2D::Change(int,int, double) { }; // nastavení hodnoty na pozici a získání hodnoty původní

použití

double dp = pa.Get(2,2); // zjištění hodnoty na dané pozici
pa.Set(3,2,1); // nastavení hodnoty na dané pozici, nemá návratovou hodnotu
dp = pa.Change(2,2,7); // nastavení nové hodnoty na dané pozici, původní přepisovaná hodnota je vrácena pro případné další použití, časově náročnější než Set

Pro zadávání a čtení je též možné použít operátor funkčního volání - operator(), který bude vracet referenci na daný prvek a je proto možné ho použít i na levé straně „rovná se“. Uživatel tak dostává přímý přístup k prvkům pole (bez kontroly). Otázkou je, co vrátit při požadavku přístupu mimo meze aktuálního pole.

double & operator()(int aX,int aY) {} // v definici třídy prototyp metody (pouze pro pole obsahující typ double

dp = pa(2,2); // zjištění hodnoty na dané pozici
pa(3,3) = 8; // zápis na danou pozici
pa(100,100) = 4; // pokus o zápis mimo rozměry pole



Krok 10 Metody pro práci s daty

První metody už byly ukázány (Resize, AddColumn ...). Metodou pro práci s polem může být například Transpozice (šla by sice realizovat pouze nastavením pomocné proměnné, ale realizujme ji fyzickým otočením – změnou matice), FlipVertical, FlipHorizontal ...

pd.Transpozice(); // provede „vnitřní“ transpozici (transponuje prvky aktuálního prvku - v objektu pd)


Pro získání prvku transponovaného, tak aby se originální prvek nezměnil, můžeme použít friend funkce (tuto funkci nevyvolá objekt jako je tomu u metody, ale objekt je jejím parametrem)

pa = Transpozice(pd); // pd se nezmění, v pa bude transponovaná hodnota pd



Krok 11 Operátory

Operátory jsou zvláštní metody, které mají dvě možnosti volání - zkrácenou a funkční („plnou“) (u některých operátorů se může chování těchto dvou mírně lišit, stejně tak jako může nastat problém při použití s ukazatelem na objekt). Pomocí funkční notace se i definují ve třídě. Jelikož prvním operandem je prvek, který si metodu/operátor „zavolá“, nemá unární operátor žádný parametr (+this), binární operátor má jeden parametr (+this). V hlavičkovém souboru

CPole2D & operator=(CPole2D const &aOrig) { return *this;} // binární operátor, umožňuje zřetězení, tělo může být i ve zdrojovém souboru
bool operator!=(CPole2D & aParam) {} // binární operátor logický, srovnání dvou prvků stejného typu
bool operator!=(double & aParam) {} // lze srovnat i s jiným typem – pokud to dává smysl
operator int(CPole2D &aParam) {} // konverzní operátor nemá návratovou hodnotu – je v jeho jméně, v tomto případě ztrátový – matice se vyjádří jedním číslem
bool operator!(void) {} // unární operátor

použití

ph[1] = ph[2] = pc; // přiřazení, které musí „umět“ zřetězení. Pozor na mělké kopie
pa = pa; // pozor na přiřazení do stejného prvku – musí se ošetřit
ph[3].operator=(pd); // „plné“ volání operátoru =
if (pa != pb) pa = pb; // logický operátor pro srovnání „rozdílnosti“
if (pa > pd) pb = pf; // srovnání „velikosti“ matic. Co je velikost záleží na definici autora
int ii = int (pb); // konverze na int. samozřejmě „ztrátová“. Může obsahovat počet prvků v matici, počet (ne)nulových prvků v matici ... - zkrátka na základě definice autora).

Matematické operátory potom mají význam u matic.

CMatice ma,mb,mc,md,me;
ma = mb = -mc + md * me; // přiřazení (zřetězené/vícenásobné), unární mínus, plus a krát (objekt * objekt)
if (ma <= mc) mb -= +me * 8; // logický operátor <= pro srovnání (záleží na definici - prvek po prvku, determinant ...), unární plus, operátory „-=“ a krát (objekt*int)
else mb -= 3*p4; // operátor „-=“, nečlenský operátor (realizován funkcí) int*objekt
unsiged f = ma != mc; // logický operátor je různé „!=“ (vrací typ bool, následně konvertován na float a přiřazen)
int i = (int)mc; // konverzní operátor (objekt se převede na int)



Krok 12 Vstup a výstup

Pomocí přetížení operátorů << a >> se realizují přetížení vstupu a výstupu prvků třídy. Jelikož prvním operandem je prvek třídy pro komunikaci „s okolím“, musí být tyto realizovány (friend) funkcí, kdy prvním operandem je třída vstupu/výstupu a druhým operandem naše třída. Tuto metodu je nutné napsat, formát vstupu a výstupu je libovolný - záleží na autorovi. Je však výhodné, pokud vstupní operátor dokáže načíst to co vystoupí z operátoru výstupního. Pokud tyto operátory napíši pro všechny třídy, mohu je kombinovat v jedné vstupní/výstupní sekvenci

definice hlavička uvnitř třídy

friend ostream & operator<<(ostream& os, CPole2D &aParam); // nemůže být členský operátor naší třídy, protože prvním operandem není typ naší třídy. Proto musí být nečlenský operátor a jelikož nemáme možnost ovlivnit třídu ostream, musí být realizován jako funkce. Aby měla funkce jednodušší práci s daty (v private sekci) označíme ji jako friend aby mohla do sekce private přistupovat

ve zdrojovém kódu

ostream& operator<<(ostream&os, CPole2D &aParam) // hlavička funkce, již žádná návaznost na třídu, kromě proměnné
{ os << “ < “ ; // výstup znaků, které usnadní orientaci ve výstupním textu
os << iX <<“ , “ << iY; // výstup několika znaků díky zřetězení (rozměrů a oddělovače)
os << // tady se vytisknou data. Tisk bude fungovat pro každý datový typ (který obsahuje 2D pole), který má tento operátor definován (přetížen)
}

použití

{cout << " ted si vypisu hodnoty me tridy " << ma << " a pro srovnani " << pb << "\n zadejte hodnoty novych prvku"<<endl; // výstup prvků je díky přetížení možný společně
cin >> ma >> pb; // i vstup je možný učinit společně/postupně
ifstream is(
"nacti.dat",ios::in); is >> mc; } //je možné použít i pro práci se soubory
}





Krok 13 dědění

Doporučuji zkoušet na verzi bez šablon. V případě, že se bude dařit, můžete opět následně předělat pro verzi se šablonami.

Dědění se používá ke znovupoužití kódu, kdy následnická třída (potomek) využívá vlastnosti předka, nebo jeho vlastnosti mírně modifikuje, rozšiřuje. V našem případě odvodíme třídu CMatice ze třídy CPole2D. Matice je v základu 2D pole, a navíc „umí“ matematické výpočty.

Musíme vybrat způsob dědění. Jelikož by bylo vhodné, aby třída CMatice uměla z hlediska uživatele to co 2D pole (transpozice, změna rozměrů ...) tak se nabízí způsob dědění public, který způsobí, že všechny public metody (interface) třídy CPole budou i veřejnými metodami (součástí interface) třídy CMatice.

Při dědění se „ztrácí“ přímý přístup k private položkám. Položky zůstávají, ale je možné k nim z potomka přistupovat pouze jako uživatel - přes interface. To vede k zdlouhavému přístupu k prvkům pole matice - a takových přístupů bude hodně. Proto je výhodné přesunout proměnné ze sekce private do sekce protected (z hlediska „jedné vrstvy“ je to stejné jako private - uživatel k těmto členům nemůže, z hlediska dědění se však (při dědění public) tyto prvky opět dostávají do sekce protected (potomkovi jsou přístupné, ale uživatelovi jsou skryté).

Přístup k metodám a datům je potom ve zděděné třídě stejný jako ve třídě bázové. Tímto dochází k „otevření“ třídy pro „okolí“ (byť omezené) a proto je nutné si tento krok promyslet, zda skutečně má přínos.

class CMatice : public CPole2D {
protected: int x,y; ...
};



Důležitým jevem je i volání konstruktorů. Platí, že se jako první volá konstruktor předka, následně konstruktory členských dat a nakonec tělo konstruktoru. (V našem případě tedy konstruktor pole (báze matice ze které vycházíme), konstruktory prvků pole (pole nemá bázovou třídu, takže pokračujeme prvky), tělo konstruktoru pole (tím dokončíme konstruktor bázové třídy a vracíme se), konstruktory prvků matice, tělo konstruktoru matice. Z tohoto postupu je zřejmé, že není vhodné nastavovat prvky až v těle konstruktoru, ale použít k tomu konstruktory prvků, protože jinak jsou nejprve volány konstruktory implicitní a následně je v těle konstruktoru prvek nastaven. Typ volaného konstruktoru pro členská data se určí mezi hlavičkou a tělem konstruktoru, pořadí volání je podle pořadí definice proměnných ve třídě.



CPole2D(void) : x(0), y(0), Pole(nullptr) {} // volání konstruktorů pro rozměry a inicializace ukazatele na pole. V těle konstruktoru není nutné v tomto případě nic dělat.

CMatice(int x, int y, double value): CPole2D(x,y,value) {} // zavolá se konstruktor bázové třídy pro vytvoření 2D pole a naplnění hodnotou - jedna metoda

CMatice(int x, int y, double value) {Resize(x,y,value);} // nejprve se zavolá implicitní konstruktor 2D pole a následně se volá Resize

destruktory se volají v opačném pořadí

Metody třídy CMatice se tvoří/píší stejným způsobem jako metody CPole2D.



Krok 14 Template - šablony

V tomto kroku (po odladění CPole2D) si proveďte kopii a pracujte s ní.

Dosud byla naše tříd CPole2D schopna „pojmout“ pouze pole proměnných jednoho typu. Jelikož je dosti problematické mít více takových tříd v rámci jednoho projektu, jsme odkázáni na jeden typ v poli. Je možné si vytvořit pole pro každý typ, který potřebujeme. Je zde ovšem problém s udržitelností kódu (nové metody, vlastnosti, oprava kódu). Proto použijeme mechanizmus šablon, kdy napíšeme šablonu (předpis, template) na základě kterého si překladač třídu pro požadovaný typ vytvoří sám.

My napíšeme šablonu pro obecný typ <typename MAT_TYP>. Jelikož se netvoří kód a je to pouze předpis, musíme všechny metody ze zdrojového souboru (cpp) přenést do hlavičkového souboru (h). V případě požadavku si daný kód překladač podle předpisu vytvoří.

Při tvorbě třídy musíme říci, že se jedná o šablonu nad daným obecným typem

template <typename MAT_TYP> class CPole2D { // definice šablonové třídy a zástupného typu, který se bude měnit
MAT_TYP ** Pole; // ukazatel dvourozměrného pole - použitý zástupný typ, který se dosadí až v případě vytváření objektu
};



metoda ve zdrojové části má prototyp

template <typename MAT_TYP> void CPole2D<MAT_TYP>::Add( CPole2D <MAT_TYP> & value)



CPole2D <int> ia; // dvourozměrné pole vygenerované pro int - vytvořila se třída s názvem CPole2D <int> => typ je součástí názvu třídy a tím ji odlišuje od ostatních ze stejné šablony
CPole2D <double> ib; // dvourozměrné pole vygenerované pro double ze stejného kódu
CPole2D <CKomplex *> ic; // prvkem pole může být cokoli, co zvládne operace, které z prvky provádíme (žádné násobení, dělení, pouze srovnání - a to ukazatel umí).



Krok 15 výjimky - ošetření chyb

zkuste v metodách vyhledat místa, která kde by mohlo docházet k chybám (alokace paměti new), nebo by mohla generovat chyby (kontrola rozměrů matic před výpočtem, alokací ...). Zkuste pro jejich řešení uplatnit mechanizmus výjimek - ve třídě „hodit“ výjimku, a ve funkci main ji „odchytit“.







Poslední změna 2011-11-1