Lekce 16 - Abstraktní třídy a rozhraní v C++

C a C++ C++ Objektově orientované programování Abstraktní třídy a rozhraní v C++

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Polymorfismus v C++, jsme dokončili naši arénu. V dalších C++ tutoriálech s ní již pracovat nebudeme, ale budeme si vytvářet nové projekty. Vzhledem k tomu, že se dostáváme k pokročilejším tématům, není možné v každém díle napsat celou aplikaci (nebo její část). Jednak proto, že probírané konstrukce vyžadují naprogramovaný základ, na kterém staví, ale také proto, že témata na sebe již nebudou tolik navazovat a spojení témat dalších lekci do jednoho dostatečně malého projektu je minimálně komplikované. Některé konstrukce si tedy pouze demonstrujeme na malých příkladech, aby se ujasnil zápis a syntaxe. V tuto chvíli byste měli mít dostatečné zkušenosti k tomu, aby vás napadly vlastní situace, ve kterých se uvedená technika hodí. Samozřejmě se nadále budu snažit uvést reálné případy použití.

Nyní se můžeme vrhnout na další téma. Tentokrát se bude jednat o abstraktní metody a třídy.

Abstraktní třída

Abstraktní třída je třída, u které nemá smysl instance. Je to z toho důvodu, že je obecná (např. třída Zvire nebo GemoetrickyTvar). Zvíře bude vždy konkrétní (tedy nějaký potomek, např. Pes) a nikdy nebudeme chtít vytvořit pouze instanci třídy Zvire, proto je lepší instanciaci zakázat. Stejně tak geometrický tvar sám o sobě nemá smysl vytvořit, vždy vytvoříme něco konkrétního - obdélník, kruh nebo mnohoúhelník.

Před tím, než se podíváme na abstraktní metody, si řekneme, jak vytvořit třídu, kterou nelze instanciovat. Řešením je nastavit všechny konstruktory jako protected. Taková třída nepůjde vytvořit, ale půjde z ní dědit. Když jsme již toto téma nakousli, tak si můžeme říci, že konstruktor lze nastavit i jako privátní. V takovém případě nepůjde třída ani instanciovat a ani z ní nepůjde dědit! Jedná se o obvyklý přístup, pokud chcete zakázat dědičnost pro nějakou třídu (ne všechny třídy jsou na dědičnost připravené). K čemu nám ale taková třída potom je? Její instanci si můžeme vytvořit ve statické metodě, protože ta má přístup k privátním členům třídy (jako jsme to dělali například u arény).

class LzeDedit
{
protected:
     LzeDedit(); // konstruktor
};

class NelzeDedit
{
private:
    NelzeDedit();
public:
    NelzeDedit vytvorInstanci();
};

NelzeDedit NelzeDedit::vytvorInstanci()
{
    return NelzeDedit();
}

Abstraktní metody

Abstraktní metoda je taková metoda, po které vyžadujeme, aby byla v potomkovi překryta, typicky v předkovi její implementace nedává smysl. Například v našem případě třídy GemoetrickyTvar. Chceme, aby tvar dokázal spočítat svoji plochu. Výpočet se ale bude lišit pro každého potomka (pro čtverec, kruh i mnohoúhelník). Metodu tedy nemá smysl do předka dávat, protože by musela být prázdná. Chceme ale využít polymorfismus, takže tato metoda v předkovi být musí. Tento problém řeší právě abstraktní metody. Abstraktní metoda samozřejmě musí být i virtuální, ale nemusí mít implementaci. Po abstraktních metodách je vyžadováno, aby byly v potomkovi překryty. Syntaxe je poněkud matoucí, protože tvůrci C++ standardu nechtěli přidávat další klíčové slovo do jazyka, a tak se abstraktní metoda značí =0 za jejím názvem.

class GeometrickyTvar
{
public:
    // abstraktní metoda
    virtual float vypocitejPlochu() = 0;
};

class Ctverec : public GeometrickyTvar
{
private:
    int hrana;
public:
    // překrytí
    virtual vypocitejPlochu();
};

// implementace překrytí
float Ctverec::vypocitejPlochu()
{
    return this->hrana * this->hrana;
}

Tím jsme nadefinovali abstraktní metodu vypocitejPlochu() a překryli ji v potomkovi. V anglické literatuře se můžete setkat s výrazem "pure-virtual" metoda. Jedná se o stejné pojmenování abstraktní metody.

Pokud má třída abstraktní metodu, potom je celá třída označena jako abstraktní a nemůže být instanciována. Nemusíme tedy řešit viditelnost konstruktorů, protože samo C++ se postará o to, aby nebyla instance vytvořena. Třídy zůstávají abstraktní do doby, než není metoda nadefinována, takže i kdybychom si vytvořili třídu VelkyGeometrickyTvar, která by dědila z GeometrickyTvar, pořád se bude jednat o abstraktní třídu, protože nepřekryla metodu vypocitejPlochu().

Implementace

Jak bylo řečeno, abstraktní metoda nemusí mít implementaci, ale může. Můžeme mít například nějakou výchozí implementaci, ale chceme se ujistit, že programátor, který tvoří potomka naší třídy, si je vědom toho, že tato metoda musí být naimplementovaná a že použil výchozí implementaci. Nebo může implementace provádět část výpočtu, který je potřeba pro každé překrytí.

Nejedná se o častý požadavek a většina běžných jazyků jako C# a Java vůbec implementaci abstraktní metody neumožňují. C++ implementaci povoluje pro případ, že by se to mohlo někomu hodit a nechává rozhodnutí na programátory, který si sám zvolí, zda tuto možnost využije.

class GeometrickyTvar
{
public:
    virtual float vypocitejPlochu() = 0;
};

// implementace abstraktní metody
float GeometrickyTvar::vypocitejPlochu()
{
    return 0;
}

class Ctverec : public GeometrickyTvar
{
private:
    int hrana;
public:
    virtual float vypocitejPlochu();
};

float Ctverec::vypocitejPlochu()
{
    // vlastní implementace
    return this->hrana * this->hrana;
}

class Primka: public GeometrickyTvar
{
public:
    virtual float vypocitejPlochu();
};

float Primka::vypocitejPlochu()
{
    // použití implementace z abstraktní třídy
    return GeometrickyTvar::vypocitejPlochu();
}

Rozhraní

Rozhraním objektu se myslí to, jak je objekt viditelný zvenku. Již víme, že objekt obsahuje nějaké metody, ty mohou být privátní nebo veřejné. Rozhraní objektu tvoří právě jeho veřejné metody a je to způsob, jakým s určitým typem objektu můžeme komunikovat. Již jsme několikrát mohli vidět jaké veřejné metody naše třída nabízí, např. u našeho bojovníka do arény. Třída Bojovnik měla následující veřejné metody:

  • bool nazivu() const;
  • float utoc(Bojovnik &druhy);
  • float getZivot() const;
  • float getMaxZivot() const;
  • float getUtok() const;
  • float getObrana() const;
  • void setZivot(float zivot);
  • Bojovnik* kopiruj() const;
  • operator string() const;

Pokud si do nějaké proměnné uložíme instanci bojovníka, můžeme na ni volat metody jako utoc() nebo getZivot(). To pořád není nic nového, že?

Většina jazyků včetně C# nebo Javy definují tzv. rozhraní. To vytvoří nový typ, který obsahuje pouze názvy metod. Každá třída, která poté dědí z rozhraní, musí tyto metody implementovat. To je velmi podobné naší abstraktní třídě, ale na rozdíl od rozhraní může abstraktní třída obsahovat atributy a implementaci metod. V ostatních programovacích jazycích rozhraní definuje čistě hlavičky metod a nic víc. Je to způsobeno tím, že ostatní jazyky (na rozdíl od C++ a Pythonu) nepovolují vícenásobnou dědičnost a tak musí věci řešit jinak. My si můžeme rozhraní nadefinovat na základě abstraktní třídy.

Například nepotřebujeme ve všech situacích vědět, že máme instanci bojovníka. V některých případech nám stačí vědět, že instance má metodu utoc(). To je rozhraní, které vyžadujeme, a nadefinujeme si jej abstraktní třídou.

class MuzeUtocit
{
public:
    virtual float utoc(Bojovnik& druhy) = 0;
};

Nyní nám stačí bojovníka podědit z abstraktní třídy MuzeUtocit a kdekoliv, kde je vyžadována metoda utoc() nám postačí, když jako typ uvedeme MuzeUtocit. Tento přístup má obrovskou výhodu - pokud budeme chtít např. vytvořit novou třídu BojoveVozidlo, program nám zůstane funkční, protože i když mu nebudeme předávat instanci bojovníka, potřebuje jen aby měl objekt metodu utoc(), což zajistíme použitím rozhraní MuzeUtocit. Z tohoto rozhraní samozřejmě v tomto případě podědíme jak bojovníka, tak bojové vozidlo. Další využití nalezneme v případě testování, kdy chceme skutečného bojovníka nahradit testovacím. Opět s tím není problém a program se nemusí měnit. Pokud bychom nechali jako typ parametru Bojovnik, musel by náš testovací bojovník vždy dědit z této třídy.

class MuzeDoAreny: public MuzeUtocit
{
public:
    virtual bool nazivu() = 0;
    virtual void obnov();
}


void souboj(MuzeDoAreny& prvni, MuzeDoAreny& druhy)
{
    while(prvni.nazivu() && druhy.nazivu())
    {
         prvni.utoc(druhy);
         if(druhy.nazivu())
             druhy.utoc(prvni);
     }
}

Nyní je jedno, jestli do metody přijde bojové vozidlo, bojovník nebo mág. Stačí, když třída bude implementovat požadované rozhraní. Pokud se na příklad pozorně podíváte, tak objevíte další problém. Metoda utoc() přijímá referenci na bojovníka, ale dostala by ji pouze instanci typu MuzeDoAreny. Pokud ale změníme parametr metody utoc(), přestaneme mít přístup k počtu životů a k obraně, protože rozhraní je nemá nadefinováno. Jak je vidět, tvořit rozhraní není tak snadné, jak se na první pohled může zdát a rozhraní MuzeDoAreny by mělo definovat další potřebné metody.

Tím je dnešní lekce u konce a příště, Šablonové třídy v C++, se podíváme na šablonové třídy.


 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Miniatura
Předchozí článek
Polymorfismus v C++
Miniatura
Následující článek
Šablonové třídy v C++
Aktivity (6)

 

 

Komentáře

Avatar
Martin Dida
Člen
Avatar
Martin Dida:14.11.2017 9:11

Ahoj.

Chcem sa spytat ohlade toho rozhrania. Dalo by sa to trochu rozsirit o implementaciu rozhrania v kode. Napr. nechapem preco v classe "MuzeUtocit" nie je metoda "utoc" deklarovana ako virtual, kedze dalej v texte udavate, ze: "Nyní nám stačí bojovníka podědit z abstraktní třídy MuzeUtocit a kdekoliv, kde je vyžadována metoda utoc() nám postačí, když jako typ uvedeme MuzeUtocit." Takze ak to spravne chapem, tak by to mala byt virtualna metoda v tej classe MuzeUtocit a nasledne je v Bojovnikovy definovana vo virtualnej metode "utoc" z bojovnika.
Mohli by ste objasnit aj ako ste mysleli tu vetu?
Ako tutorial je velmi dobry zalozeny hned na priamom programovani v C++, co je super. Akurat v tejto sekcii rozhranii sa akurat trochu stracam. Mozno to viac opisat kodom, aby clovek videl ako sa to celkovo implementuje.

Dik :) Martin

 
Odpovědět 14.11.2017 9:11
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Martin Dida
patrik.valkovic:14.11.2017 10:43

Ahoj,
metoda, která je deklarována jako abstraktní ( = 0 na konci), je automaticky brána jako virtuální a virtual být uvedeno může, ale nemusí. Je to logické, protože po potomkovi chceme, aby tuto metodu implementoval, tudíž nutně musí být virtuální.
Virtual ve zděděných třídách se psát opět může a nemusí, jakmile je metoda deklarovaná jednou jako virtuální, už je virtuální napořád.

Odpovědět 14.11.2017 10:43
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Martin Dida
Člen
Avatar
Martin Dida:14.11.2017 13:35

Ahoj
To chapem. Avsak po vytvoreni classy MuzeUtocit a pridanim tejto classy do Bojovnika
(teda deklarovanim bojovnika "class Bojovnik : public MuzeUtocit") stale dostavam chybu: "error C2504: 'CanAttack' : base class undefined" CanAttack == MuzeUtocit, kedze to pisem po anglicky.
Buildujem to vo Visual Studio 2010.

Dik :)

 
Odpovědět 14.11.2017 13:35
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Martin Dida
patrik.valkovic:14.11.2017 14:14

Nevidí to na třídu "MuzeUtocit", nezapoměl jsi ji includovat?

Odpovědět 14.11.2017 14:14
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 4 zpráv z 4.