Jako obvykle, i tento díl začne stručným shrnutím toho, co jsme se naučili v díle předchozím. Nyní byste tedy měli rozumět pojmům konstruktor, destruktor a modifikátory přístupu. Samozřejmě byste také měli umět konstruktor a destruktor napsat a v každé třídě vhodně zvolit pro jednotlivé atributy a metody to, zda budou privátní či veřejné.
Dnes na předchozí díl navážeme dědičností. Dovíte se tedy, co to dědičnost je, proč jí používáme, jaké jsou druhy dědičnosti atd. Vysvětlíme si také modifikátor přístupu protected.
Co je to dědičnost?
Pokud jste četli minulý díl (já pevně doufám, že ano), jistě máte napsanou naší třídu Zamestnanec. Jistě se mnou budete souhlasit, když řeknu, že zaměstnanec je dosti široký pojem. Zaměstnancem může být kdokoliv – uklízečka, sekretářka, programátor, revizor, dělník apod. Pokud budeme chtít v našem programu nějakým zásadnějším způsobem rozlišit, o jakého zaměstnance se jedná, budeme muset napsat každou třídu zvlášť – tedy jednu pro uklízečku, druhou pro sekretářku atd.
Mějme však stále na paměti, že se pořád jedná o zaměstnance. Nepochybně je také pravda, že každý zaměstnanec má svoje jméno, svůj plat, počet let praxe apod. To jsou obecné věci, které má každý zaměstnanec, bez ohledu na to, jakou práci vykonává. Samozřejmě najdeme ale i věci, které už jsou pro každou profesi individuální – dělník může mít atribut, podle kterého poznáme, v jaké části továrny pracuje, u programátora můžeme evidovat, v jakém jazyce nejčastěji programuje atd.
Co jsem tedy chtěl výše uvedenými odstavci naznačit? Nic jiného než to, že pokud bychom psali třídu pro každý druh zaměstnance, určitě by se nám některé atributy a metody opakovali – psali bychom je vícekrát, přitom by dělaly to samé. To je bezesporu značně nevýhodné – úplně zbytečně budeme mít mnohem delší kód. No a čím delší kód máme, tím máme více prostoru na to, abychom v něm nasekali více chyb. Pokud bychom si i přesto dali tu práci a kód tímto špatným způsobem napsali, další problémy se nám ani tak nevyhnou. Pokud budeme například chtít u zaměstnance zavést nový atribut (např. telefonní číslo), budeme muset tento atribut dopsat do všech tříd, které reprezentují nějaký konkrétní druh zaměstnance. To samé platí, pokud budeme chtít nějaký atribut odebrat – opět musíme projít všechny třídy a atribut několikrát smazat.
Jak tedy řešit všechny tyto problémy? Asi každého podle nadpisu tohoto dílu napadne, že tajemství je ukryto v použití dědičnosti.
Nejlepším vysvětlením bude asi příklad s názornou ukázkou. Dejme tomu tedy, že chceme vytvořit třídu Programator. Položme si tedy nyní otázku – je programátor zaměstnanec? Má programátor jméno, plat, počet let praxe atd? Samozřejmě že ano. Naše třída zaměstnanec už všechny tyto atributy má, čili není nutné je psát znovu. O všechno, co potřebujeme, se postará následující zápis:
class Programator : public Zamestnanec
{
};
Tím říkáme následující – třída Programator dědí (veřejně) z třídy Zamestnanec. To znamená, že třída programátor nyní obsahuje všechny metody a atributy, jako třída Zamestnanec. To, jestli jsou tyto zděděné metody a atributy veřejné, chráněné či privátní je věc druhá, na kterou se za okamžik podíváme.
Jak organizovat třídy do souborů
Nejprve bych ale rád udělal malou odbočku, protože někoho z Vás možná napadla otázka, kam vlastně tuto novou třídu napsat – tím myslím do jakého souboru. Možností je více. Ta nejjednodušší, nejpohodlnější, ale zároveň ne příliš vhodná možnost je napsat tuto třídu pod třídu Zamestnanec, čili do souboru Zamestnanec.h.
Druhou možností, kterou jsem v tomto případě také použil, je vytvořit nový soubor s názvem Programator.h a třídu napsat do něj. Samozřejmě nesmíme zapomenout v tomto souboru úplně nahoru napsat
#include "Zamestnanec.h"
V souboru main.cpp pak už stačí nahoře mít jen
#include "Programator.h"
Pak bude vše fungovat jak má a zároveň budeme mít třídy samostatně v jednotlivých souborech, což nám pak pomůže v rychlejší orientaci v celém projektu.
Pokud Vám ani tento způsob nevyhovuje, můžete také oddělit těla metod od zbytku třídy. Tím myslím následující: v hlavičkovém souboru budete mít pouze třídu, v níž napíšete její atributy, ale u metod nebudete psát jejích tělo, napíšete pouze jejich “hlavičku” a ukončíte středníkem. V nějakém jiném .cpp souboru pak implementaci těchto metod doplníte. Mohlo by to vypadat zhruba takto:
class NejakaTrida
{
private:
int atribut1, atribut2;
public:
NejakaTrida(); // Konstruktor
int Metoda1(int p1, int p2); // Priklad nejake metody
};
#include "NejakaTrida.h"
NejakaTrida::NejakaTrida()
{
// telo konstruktoru
}
int NejakaTrida::Metoda1(int p1, int p2)
{
// telo metody
return p1 * p2;
}
Vidíte, že v tomto případě vždy nejprve píšeme návratový typ metody (u konstruktoru žádný není), poté název třídy a přes dvojtou dvojtečku přistoupíme k potřebné metodě.
Po malé odbočce, ve které jsme si udělali jasno, jak třídy organizovat do souborů, se nyní vrátíme zpět k dědičnosti.
Modifikátory přístupu a dědičnost
Bez velké námahy máme v tuto chvíli napsanou třídu Programator. Doplníme do ní další atribut, podle kterého poznáme, jaký programovací jazyk programátor nejčastěji používá. Dopíšeme ji také konstruktor, ve kterém mimo jiné nastavíme hodnotu tohoto atributu.
class Programator : public Zamestnanec
{
private:
string progJazyk;
public:
Programator(int rokNar, int praxe, int plat, string jm, bool _jeMuz, string jazyk)
{
rokNarozeni = rokNar;
pocetLetPraxe = praxe;
vysePlatu = plat;
jmeno = jm;
jeMuz = _jeMuz;
progJazyk = jazyk;
}
};
Pokud píšete kód ve Visual Studiu, pravděpodobně se Vám, stejně jako mě, prvních pět atributů (tedy těch zděděných) podtrhlo červeně a kód ani nelze zkompilovat. V čem je tedy problém a jak ho řešit?
Řešení je velmi snadné a rychlé. Ve třídě Zamestnanec změňte modifikátor přístupu private na protected. Teď už by červené podtržení atributů mělo zmizet, ale kód zkompilovat ještě nepůjde. Ve třídě Zamestnanec chybí bezparametrický, defaultní konstruktor – proto jej dopište, v těle klidně nemusí být vůbec nic. Jde o to, že jakmile jsme vytvořili konstruktor s parametry, defaultní bezparametrický konstruktor si již C++ nevytvoří. Teď už by kompilace měla proběhnout bez problémů.
Je však třeba udělat pořádek v tom, jak to vlastně s těmi modifikátory přístupu v souvislosti s dědičností je, proto se podívejte na následující tabulku.
Typ dědičnosti_____________
Typ atributu (metody) |
Soukromá dědičnost |
Chráněná dědičnost |
Veřejná dědičnost |
soukromý (private) |
položky budou přístupné jen přes základní třídu |
položky budou přístupné jen přes základní třídu |
položky budou přístupné jen přes základní třídu |
chráněný (protected) |
soukromé položky |
chráněné položky |
chráněné položky |
veřejný (public) |
soukromé položky |
chráněné položky |
veřejné položky |
Nejprve se pořádně podívejte na poslední sloupec, který se vztahuje k veřejné dědičnosti, kterou jsme použili i my v našem příkladu. Tabulka nám říká, že privátní atributy třídy Zamestnanec budou ve třídě Programator přístupné pouze přes základní, čili bázovou třídu – třídu Zamestnanec. Chráněné a veřejné položky zůstanou stejné – tedy chráněné chráněnými a veřejné veřejnými. To tedy vysvětluje, proč byly atributy podtrženy červeně předtím, než jsme v bázové třídě změnili jejich modifikátor přístupu.
Soukromá a chráněná dědičnost
Z tabulky jste jistě sami pochopili, že v C++ není pouze veřejná dědičnost, ale také soukromá a chráněná. V tomto díle se jí nebudu dále zabývat, protože se používá velmi zřídka. Ukážu jen její zápis, který ale není nijak složitý – stačí pouze změnit klíčové slovo public na jiné.
class Programator : protected Zamestnanec // Chráněná dědičnost
class Programator : private Zamestnanec // Soukromá dědičnost
Ještě bych rád zdůraznil, že v případě použití veřejné (public) dědičnosti je nutné klíčové slovo public uvést, protože jinak Vám kompilátor nezahlásí chybu, ale použije soukromou dědičnost, což může vést k dost dlouhému hledání chyby.
Jak je to s voláním konstruktorů?
Další věcí, kterou je nutno vědět, je způsob, jak se při použití dědičnosti a následném vytváření objektů volají konstruktory. Pro jednoduchost předpokládejme třídy A, B, C, přičemž třída C dědí ze třídy B, která zase dědí z třídy A. Pokud poté vytvoříme instanci třídy C, nezavolá se jen konstruktor třídy C, ale zavolá se i konstruktor třídy A a B a to v následujícím pořadí:
- Nejprve se volá konstruktor třídy A
- Pak konstruktor třídy B
- Nakonec konstruktor třídy C
Jednoduše řečeno, pokud nějaká třída dědí z jiné třídy, a ta zase z jiné, vždy se konstruktory volají podle hierarchie dědičnosti.
Přesně naopak se volají destruktory. Dochází-li k ukončení platnosti objektu třídy C, zavolá se destruktor třídy C, pak destruktor třídy B a nakonec destruktor třídy A.
Obě tyto skutečnosti si můžete jednoduše ověřit. Stačí si napsat jednoduché třídy a do jejich konstruktorů (destruktorů) napsat kód, který pouze vypíše nějakou hlášku – sami uvidíte, v jakém pořadí budou jednotlivé hlášky vypsány.
Vícenásobná dědičnost
Stejně okrajově, jako jsem zmínil chráněnou a soukromou dědičnost, zmíním i vícenásobnou dědičnost. Nejde o nic jiného než o to, že daná třída dědí z více tříd najednou – tedy třída A bude současně dědit z třídy B a C. Syntaxe je následující:
class A : public B, public C
Samozřejmě i v tomto případě je možno použít chráněnou či soukromou dědičnost. Vícenásobná dědičnost je však také málo používaná věc, neboť je s ní spjato mnoho problémů – například dědění metod stejného názvu z více tříd. Pokud někdo máte zkušenosti například s jazykem Java, určitě více, že tam vícenásobná dědičnost neexistuje, stejně jako v jazyce C#. Tyto jazyky ale mají jinou věc, kterou C++ nedisponuje – tzv. rozhraní, které je oproti vícenásobné dědičnosti v C++ používáno celkem často.
Zpátky k veřejné dědičnosti
Po dvou menších odbočkách se opět vrátíme k dědičnosti, která se v C++ nejčastěji používá. Známe již syntaxi jak dědičnost zapsat a víme, co to vlastně je a jaké jsou výhody. Dále máme napsán i konstruktor odvozené třídy Programator, takže nám nic nebrání v tom, abychom si vytvořili objekt této třídy a zavolali nějaké metody.
int main()
{
Programator p(1985, 4, 32000, "Jan Krysa", true, "C++");
cout << "Programator " << p.VratJmeno() << " ma plat " << p.VratPlat() << ".\n";
return 0;
}
Je možné, že metodu VratJmeno() nemáte napsanou, proto si ji dopište – samozřejmě do třídy Zamestnanec, třída Programator ji zdědí.
Sami již vidíte, že naše třída Programator, kterou jsme napsali bez velkého úsilí, umí to, co třída Zamestnanec, a navíc u ní máme atribut progJazyk.
Předpokládám, že v tuto chvíli by Vám dědičnost neměla být úplně cizí, ale možná Vám bude nějakou dobu trvat, než si s ní trochu více porozumíte. Vždy je důležité také zvážit, zda se vůbec dědičnost hodí použít. Obecně vzato platí, že pokud je daná třída jakýmsi specifickým upřesněním jiné třídy, jde o vztah dědičnosti. V našem případě to samozřejmě platí – programátor je specifickým druhem zaměstnance. Stejně tak je například čtverec specifickým druhem geometrického obrazce. Zkusme ale například vzít vztah dveře a okno. Hodí se zde dědičnost? Někdo by řekl, že obojí se dá otevřít a zavřít, klidně mohou mít stejnou barvu, ale ani přesto bych v tomto případě dědičnost nepoužil. Dveře nejsou specifickým upřesněním okna, což neplatí ani naopak. Jiná věc by byla, vytvořit třídu, která by reprezentovala nějaké věci v bytě a od té pak podědit dveře a okno. Mezi dveřmi a oknem je však použití dědičnosti krajně nevhodné. Možná se smějete, že jsem uvedl zcela banální příklad a že to je přece jasné, kdy dědičnost použít a kdy ne, ale v některých případech to až tak jasné není.
Pro dnešní díl je to vše. Měli byste nyní vědět, co je to dědičnost, jaká je její syntaxe, jaké jsou její druhy a měli byste mít alespoň nějaké základní povědomí o tom, ve kterých případech ji použít. V příštím díle budeme stále ještě s dědičností pracovat, v krátkosti se ještě vrátíme ke konstruktorům a pak se k nám vrátí pointery, ne však na primitivní datové typy, ale pointery na objekty. Také se budeme věnovat virtuálním metodám.