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