7. Základy OOP VII
  1. V této kapitole budeme vyvíjet složitější konzolovou aplikaci. Jedná se o jednoduchou databázi, ve které si ukážeme komplexnější použití virtuálních funkcí. Nejprve vytvoříme deklaraci a implementaci třídy osoba. Tato třída bude obsahovat dvě chráněné datové složky a jednu virtuální metodu.

  2. class osoba {
    protected:
      char jmeno[25];
      int plat;
    public:
      virtual void zobraz(void);
    };
    void osoba::zobraz(void){
      cout << "osoba::zobraz - chybějící metoda v odvozené třídě\n";
    }
    Implementace této třídy je snadná, bude obsahovat pouze implementaci virtuální metody zobraz. Tato třída bude základem pro odvozování dalších specializovaných tříd, ale nikdy ji nebudeme používat (budeme ji používat pouze jako třídu předka). Z tohoto důvodu by metoda zobraz neměla být nikdy volána a do její implementace vložíme pouze výpis signalizace chyby.
    Dále vytvoříme tři odvozené třídy. Jsou to třídy vedouci, programator a sekretarka. Všechny obsahují metodu zobraz, která je stejná jako tato metoda v třídě předka. Jsou to tedy virtuální metody.
    class vedouci : public osoba {
      char titul[25];
    public:
      void inicializace(char jm[], int pl, char ti[]);
      void zobraz(void);
    };
    class programator : public osoba {
      char titul[25];
      char jazyk[25];
    public:
      void inicializace(char jm[], int pl, char ti[], char ja[]);
      void zobraz(void);
    };
    class sekretarka : public osoba {
      char tesnopis;
      int rychlost_psani;
    public:
      void inicializace(char jm[], int pl, char te, int ry);
      void zobraz(void);
    };
    void vedouci::inicializace(char jm[], int pl, char ti[]){
      strcpy(jmeno, jm);
      plat = pl;
      strcpy(titul, ti);
    }
    void vedouci::zobraz(void){
      cout << "Vedoucí     --> " << jmeno << " má plat " << plat <<
              " a je " << titul << ".\n\n";
    }
    void programator::inicializace(char jm[], int pl, char ti[], char ja[]){
      strcpy(jmeno, jm);
      plat = pl;
      strcpy(titul, ti);
      strcpy(jazyk, ja);
    }
    void programator::zobraz(void){
      cout << "Programátor --> " << jmeno << " má plat " << plat <<
              " a je " << titul << ".\n\n";
      cout << "                " << jmeno<<" programuje v "<<jazyk<<" .\n\n";
    }
    void sekretarka::inicializace(char jm[], int pl, char te, int ry){
      strcpy(jmeno, jm);
      plat = pl;
      tesnopis = te;
      rychlost_psani = ry;
    }
    void sekretarka::zobraz(void){
      cout << "Sekretářka  --> " << jmeno << " má plat " << plat << ".\n\n";
      cout << "                " << jmeno << " píše rychlostí " <<
      rychlost_psani << " znaků za minutu a ";
      if (!tesnopis) cout << "ne";
      cout << "ovládá těsnopis.\n\n";
    }
    V každé z těchto tříd nalezneme metodu inicializace, která provádí inicializaci datových složek třídy a virtuální metodu zobraz vypisující data třídy a to pro každou třídu jiným způsobem.
    Vytvořené třídy nyní použijeme v jednoduchém programu. Na začátku programu je deklarace pole ukazatelů o 10 prvcích, typu ukazatel na osoba. Do tohoto pole budeme ukládat ukazatele na odvozené třídy. V programu alokujeme několik objektů odvozených tříd, inicializujeme je a ukazatele na ně vložíme do našeho pole. Na závěr programu informace uložené v objektech vypíšeme. Program si prostudujte a vyzkoušejte.
    osoba *stav[10];
    int main(int argc, char **argv)
    {
      vedouci *ved;
      programator *prog;
      sekretarka *sekr;
      ved = new vedouci;
      ved->inicializace("Karel Velký", 27000, "prezident");
      stav[0] = ved;
      prog = new programator;
      prog->inicializace("Jan Novák", 21000, "analytik", "Pascal");
      stav[1] = prog;
      prog = new programator;
      prog->inicializace("Jiří Novotný", 17000, "programaror", "C++");
      stav[2] = prog;
      sekr = new sekretarka;
      sekr->inicializace("Marie Hezká", 10370, 1, 350);
      stav[3] = sekr;
      ved = new vedouci;
      ved->inicializace("Jarmila Stará", 17000, "vedoucí účetní");
      stav[4] = ved;
      prog = new programator;
      prog->inicializace("Jan Kluzký",11000,"pomocný programator", "Pascal");
      stav[5] = prog;
      for (int index = 0; index < 6; index++)
        stav[index]->zobraz();
      return 0;
    }
  3. Metodu zobraz ve třídě osoba můžeme deklarovat také jako čirou virtuální metodu. Čirá virtuální metoda nemá implementaci a nezamýšlíme ji volat. Deklarujeme ji konstrukcí prototyp_metody = 0; v našem případě jde o deklaraci

  4. virtual void zobraz(void) = 0;
    Třída, která obsahuje alespoň jednu čirou metodu se v terminologii C++ označuje jako abstraktní. Překladač nedovoluje definovat instance abstraktních tříd. Čiré metody nemají definiční deklaraci. Pokus o volání čiré metody skončí chybou. Změňte třídu osoba na abstraktní a vyzkoušejte.
  5. V tomto zadání budeme pokračovat ve vývoji databáze zaměstnanců. Následuje deklarace a implementace dalších dvou tříd, které využijeme k vytvoření spojového seznamu zaměstnanců. Povšimněte si, že prvky spojového seznamu neobsahují data, ale ukazatele na třídu osoba, kterou jsme vytvořili v předchozím programu, a můžeme tedy vytvořit spojový seznam prvků tříd osoba a to bez nutnosti modifikace této třídy. V tomto souboru si povšimněte použití dopředné deklarace třídy seznam_zamest (musíme ji použít, neboť obě třídy se odkazují na sebe navzájem). Dále si povšimněte posledního řádku v deklaraci třídy prvek_seznamu (friend class seznam_zamest;), který dává třídě seznam_zamest volný přístup k položkám třídy prvek_seznamu. Je to nutné, protože metoda pridej_osobu musí přistupovat k položce dalsi.

  6. class seznam_zamest;                         // Dopředná deklarace
    class prvek_seznamu {                         // Jeden prvek zřetězeného seznamu
      osoba *data;
      prvek_seznamu *dalsi;
    public:
      prvek_seznamu(osoba *novy_zamest){
        dalsi = NULL;
        data = novy_zamest;
      };
      friend class seznam_zamest;
    };
    class seznam_zamest{                          // Zřetězený seznam
      prvek_seznamu *zacatek;
      prvek_seznamu *konec;
    public:
      seznam_zamest() {zacatek = NULL;}
      void pridej_osobu(osoba *novy_zamest);
      void zobraz_seznam(void);
    };
    void seznam_zamest::pridej_osobu(osoba *novy_zamest){
      prvek_seznamu *pom;
      pom = new prvek_seznamu(novy_zamest);
      if (zacatek == NULL)
        zacatek = konec = pom;
      else {
        konec->dalsi = pom;
        konec = pom;
      }
    }
    void seznam_zamest::zobraz_seznam(void){
      prvek_seznamu *pom;
      pom = zacatek;
      do {
        pom->data->zobraz();
        pom = pom->dalsi;
      } while (pom != NULL);
    }
    Databáze zaměstnanců je realizována pomocí spojového seznamu, který vytváříme prostřednictvím našich dvou nových tříd. Program je jednoduchý a nepotřebuje žádné vysvětlení. Prostudujte si jej a vyzkoušejte jej.
    seznam_zamest seznam;
    int main(int argc, char **argv)
    {
      vedouci *ved;
      programator *prog;
      sekretarka *sekr;
      ved = new vedouci;
      ved->inicializace("Karel Velký", 27000, "prezident");
      seznam.pridej_osobu(ved);
      prog = new programator;
      prog->inicializace("Jan Novák", 21000, "analytik", "Pascal");
      seznam.pridej_osobu(prog);
      prog = new programator;
      prog->inicializace("Jiří Novotný", 17000, "programaror", "C++");
      seznam.pridej_osobu(prog);
      sekr = new sekretarka;
      sekr->inicializace("Marie Hezká", 10370, 1, 350);
      seznam.pridej_osobu(sekr);
      ved = new vedouci;
      ved->inicializace("Jarmila Stará", 17000, "vedoucí účetní");
      seznam.pridej_osobu(ved);
      prog = new programator;
      prog->inicializace("Jan Kluzký",11000,"pomocný programator", "Pascal");
      seznam.pridej_osobu(prog);
      seznam.zobraz_seznam();
      return 0;
    }
  7. Do našeho programu přidejte deklaraci a implementaci nové třídy nazvané poradce (přidané složky si zvolte) a přidejte příkazy na vyzkoušení použití nové třídy.
  8. V předchozí kapitole jsme se seznámili s vícenásobnou dědičností. Při vícenásobné dědičnosti mohou ale vznikat problémy. Např. pro vstupy a výstupy v C++ používáme datové proudy. Základem datových proudů je třída ios. Tato třída definuje vlastnosti společné všem datovým proudům (stavové příznaky, formátovací příznaky apod.). Od třídy ios je odvozena řada specializovanějších tříd. Jedná se také o třídy istream a ostream, které definují vstupní a výstupní datové proudy. Společným potomkem těchto obou tříd je třída iostream pro proudy, které umožňují zároveň vstup i výstup dat. iostream tedy dědí vše od istream a ostream a nic dalšího nepřidává. Z pravidel vícenásobného dědění vyplývá, že iostream bude obsahovat dva zděděné podobjekty třídy ios a tedy i dvakrát formátovací a stavové příznaky, což není vůbec vhodné. Jazyk C++ nabízí řešení v podobě tzv. virtuálního dědění. To zajistí, že vícekrát zděděné prvky se sloučí. Deklarujeme-li třídu ios jako virtuálního předka tříd istream a ostream, bude jejich společný potomek iostream obsahovat pouze jeden podobjekt typu ios.

  9. Virtuálního předka vytvoříme tak, že při specifikaci předka použijeme klíčové slovo virtual. Následuje příklad použití:
    class a {
      double x;
    public:
      a(){};
    };
    class aa : public virtual a {
      double a;
    public:
      aa() {};
    };
    class ab : public virtual a {
      double b;
    public:
      ab() {};
    };
    class X : public aa, public ab {
      double xx;
    };
    Instance třídy X bude nyní obsahovat pouze jeden podobjekt třídy a. Vyzkoušejte.
  10. Jednou z výhod, které nám C++ nabízí, je možnost přetěžovat nejen funkce, ale i převážnou většinu operátorů. Nelze přetěžovat operátory: . (tečka), .* (tečka_hvězdička), :: (dvě dvojtečky), ? : (podmíněný výraz), sizeof, typeid, dynamic_cast, static_cast, reinterpret_cast a const_cast. Z hlediska přetěžování lze rozdělit operátory na tři skupiny. První skupinu tvoří operátory, které můžeme přetěžovat pouze jako nestatické metody objektových typů. Patří sem operátory () (volání funkce), [] (indexování), -> (nepřímého přístupu), = (prosté přiřazení) a operátor přetypování (typ). Druhou (nejrozsáhlejší) skupinu tvoří operátory, které lze přetěžovat jako nestatické metody objektových typů nebo jako řadové funkce. Jsou to všechny operátory, které nejsou v ostatních skupinách a nejsou operátorem, který nelze přetěžovat. Tyto operátory musí mít alespoň jeden operand objektového nebo výčtového typu. Poslední skupinu tvoří operátory pro správu paměti new a delete. Platí pro ně zvláštní pravidla. Nelze změnit chování operátorů pro argumenty standardních datových typů. Pro přetěžování operátorů používáme také termín homonyma operátorů. Nejprve se seznámíme s obecnými pravidly přetěžování:
  11. Mimo těchto zásad, které jsou dány definicí jazyka, je vhodné při definování homonym operátorů dodržovat následující omezení: Nejprve se budeme zabývat přiřazovacími operátory. Operátor prostého přiřazení (=) definuje překladač implicitně, kdykoli je třeba. Prostý přiřazovací operátor můžeme přetěžovat pouze jako nestatickou metodu objektového typu, zatímco složené operátory (např. += apod.) můžeme přetěžovat i jako funkce. Přiřazovací operátor ve třídě X je metoda s prototypem
    X& X::operator=(X&)     případně        X& X::operator=(const X&)
    Je-li a instance třídy X, znamená zápis    a = b;  totéž jako    a.operator=(b);
    Implicitní verze přiřazovacího operátoru řeší přiřazování hodnot objektů prostým okopírováním jednoho objektu do druhého. To nám přestane vyhovovat např. ve chvíli, kdy naše objekty budou rozděleny na několik částí svázaných navzájem ukazateli. V následujícím příkladu je ukázka třídy umožňující pracovat s řetězci.
    class retezec {
      int delka;
      char *text;
      void Platny(const char* =(const char*)2, int = 0) const;
      retezec& Prirad(const char*, int);
      retezec& Pridej(const char*, int);
      friend ostream& operator<< (ostream&, const retezec&);
    public:
      retezec() {text = NULL;};                         //NULL indikuje ještě nepřiřazenou hodnotu řetězci
      retezec(const char *);
      retezec(const retezec&);
      retezec(int, const char * s) {
        delka = strlen(s);
        text = (char*)s;
      };
      ~retezec(){if (text) delete text;};
      retezec& operator= (const char* s) {return Prirad(s, strlen(s));};
      retezec& operator= (const retezec& S) {return Prirad(S.text, S.delka);};
      retezec& operator+= (const char* s) {return Pridej(s, strlen(s));};
      retezec& operator+= (const retezec& S) {return Pridej(S.text, S.delka);};
    };
    retezec::retezec(const char *s) {
      Platny(s, 1);
      delka = strlen(s);
      text = new char[delka+1];
      strcpy(text, s);
    }
    retezec::retezec(const retezec& S) {
      S.Platny();
      delka = S.delka;
      text = new char[delka+1];
      strcpy(text, S.text);
    }
    void retezec::Platny(const char* s, int i) const {    //pomocná metoda kontrolující korektnost operací
      if (s && (i || text)) return;
      cerr << "\n\nPoužití řetězce bez hodnoty\n\n";
      abort();
    }
    retezec& retezec::Prirad(const char* s, int i) {
      Platny(s, 1);
      if (text) delete text;
      delka = i;
      text = new char[delka+1];
      strcpy(text, s);
      return *this;
    }
    retezec& retezec::Pridej(const char* s, int i) {
      Platny(s);
      if (*s){
        char* T = new char[delka+ i+1];
        strcpy(T, text);
        strcpy(T+delka, s);
        delka += i;
        delete text;
        text = T;
      }
      return *this;
    }
    inline ostream& operator<< (ostream& o, const retezec& s) {
      o << (void*)s.text << ": " << s.delka << ">>" << s.text << "<<\n";
      return o;
    }
    int main(int argc, char **argv)
    {
      retezec a = "Karel";
      retezec b("Milada");
      retezec bb(b);
      retezec c;
      const retezec d(1, "David");
      cout << "a="<<a<<"b="<<b<<"c="<<c<<"d="<<d;
      c = a;
      cout << "\nc2="<<c;
      cout << "bb2="<< (bb = "Bohuslav ");
      bb += d;
      cout << "bb3=" << bb;
      cout << "bb4=" << (bb += " Božena");
      a = bb = NULL;                                      // nedovolená operace, bude vypsána signalizace chyby
      return 0;
    }
    Ukazatelem s hodnotou NULL v této ukázce označujeme neinicializovanou instanci. Je výhodné, pokud můžeme definovat neinicializující konstruktor tak, aby operátory, které chtějí danou proměnnou použít, uměly poznat, že dotyčná proměnná nemá přiřazenou hodnotu, a na tuto skutečnost nás nějakým způsobem upozornili. Dále si povšimněte dvouparametrického konstruktoru, u jehož prvního parametru není uveden žádný identifikátor. Tento parametr nám slouží pouze k tomu, abychom odlišili daný konstruktor od druhého konstruktoru, jehož parametrem je také textový řetězec. Tím, že jsme neuvedli u parametru jméno, jsme překladači naznačili, že dotyčný parametr nehodláme používat a že nás na jeho nepoužití překladač nemá upozorňovat. U tohoto konstruktoru nealokujeme pro inicializační text místo v hromadě, ale nasměrujeme ukazatel přímo na inicializační text (v tomto případě text nesmíme změnit).
    V ukázce jsou vytvořeny i operátory přiřazení. Jsou ve dvou verzích a to jak pro klasické textové řetězce, tak i pro řetězce právě deklarovaného typu. Těla funkcí u těchto dvou verzí si jsou velmi podobná, a proto jsme vytvořili pomocné funkce realizující společnou část algoritmu. Prostudujte si implementaci uvedené třídy a vyzkoušejte.
  12. Podobně jako u operátoru přiřazení můžeme přetěžovat i základní binární operátory (+ = * / % > < >= <= == != && || | & ^ << >>). Identifikátor operátoru je zde tvořen klíčovým slovem operátor, za nímž následuje symbol daného operátoru, který může být od slova operator oddělen libovolným počtem mezer. Pokud binární operátor definujeme jako normální funkci musí mít dva parametry a alespoň jeden z nich musí být objektového nebo výčtového typu. U operátoru definovaného jako metoda je jeho levým argumentem instance, jejíž metodou operátor je, takže v definici již deklarujeme pouze jeden parametr (pravý operand). Je vhodné, když binární operátory nemění hodnoty svých operandů a výsledek je předáván hodnotou (použití odkazu z mnoha důvodů není vhodné). Použití binárního operátoru + si ukážeme na rozšíření předchozí třídy (retezec). Do třídy přidáme:

  13. friend retezec operator+ (const retezec&, const retezec&);
    a tuto spřátelenou funkci implementujeme takto:
    inline retezec operator+ (const retezec& a, const retezec& b){
      retezec pom = a;
      return pom += b;
    }
    Do hlavního programu vložíme na vyzkoušení např. tyto příkazy:
    retezec vyrok;
    vyrok = a + " a " + b;
    cout << ("Hříšníci " + vyrok + "!!!!");
    Mohli bychom deklarovat i operátory se smíšenými parametry:
    friend retezec operator+ (const char *, const retezec&);
    friend retezec operator+ (const retezec&, const char *);
    ale není to nutné. Ve třídě jsme definovali konverzní konstruktor s parametrem typu char *. Překladač díky tomu umí zkonstruovat pomocný objekt, kterému přiřadí hodnotu předávaného řetězce a který pak předá operátoru jako skutečný parametr. Dodefinováním těchto dvou homonym operátoru sčítání nepřidáme programu žádné nové, dříve neexistující funkce. Vyzkoušejte si použití výše uvedeného operátoru sčítání.
7. Základy OOP VII