4. Základy OOP IV
  1. Následující program je náš první příklad, který používá ochranu dat. Jestliže jsou datové složky a metody třídy předka volně dostupné i ve třídách potomků, dává to programátorům větší možnosti při odvozování nových tříd. To zajistíme vložením klíčového slova protected před deklaraci těchto složek. Toto bylo provedeno u deklarace třídy vozidlo. U obou odvozených tříd je před deklaraci složek přidáno klíčové slovo private (implicitně jsou všechna data třídy soukromá a přidání tohoto klíčového slova tedy nemá žádný význam). Souhrnně můžeme popsat význam těchto klíčových slov takto:
  2. Po provedení těchto změn jsou v metodách odvozených tříd dostupné i složky z třídy předka. Vyzkoušejte.
    class vozidlo {
    protected:
      int kola;
      float vaha;
    public:
      void inicializace(int nova_kola, float nova_vaha);
      int ziskej_kola(void){return kola;}
      float ziskej_vahu(void){return vaha;}
      float vaha_na_kolo(void){return vaha/kola;}
    };
    class automobil : public vozidlo {
    private:
      int osob;
    public:
      void inicializace(int nova_kola, float nova_vaha, int lidi = 4);
      int pasazeru(void){return osob;}
    };
    class nakladni : public vozidlo {
    private:
      int osob;
      float naklad;
    public:
      void inic_nakl(int kolik = 2, float max_naklad = 12000.0);
      float vykonost(void);
      int pasazeru(void){return osob;}
    };
    void vozidlo::inicializace(int nova_kola, float nova_vaha){
      kola = nova_kola;
      vaha = nova_vaha;
    }
    void automobil::inicializace(int nova_kola, float nova_vaha, int lidi){
      osob = lidi;
      vozidlo::inicializace(nova_kola, nova_vaha);
    }
    void nakladni::inic_nakl(int kolik, float max_naklad){
      osob = kolik;
      naklad = max_naklad;
    }
    float nakladni::vykonost(void){
      return naklad / (naklad + ziskej_vahu());
    }
    int main(int argc, char **argv)
    {
      vozidlo unicykl;
      unicykl.inicializace(1, 7.5);
      cout << "Unicykl má " << unicykl.ziskej_kola() << " kol.\n";
      cout << "Zátěž na kolo unicyklu je " << unicykl.vaha_na_kolo()<< endl;
      cout << "Váha unicyklu je " << unicykl.ziskej_vahu() << " kg.\n";
      automobil sedan;
      sedan.inicializace(4, 1500.0, 5);
      cout << "Sedan má " << sedan.pasazeru() << " osob.\n";
      cout << "Sedan má zátěž " << sedan.vaha_na_kolo() << " kg na kolo.\n";
      cout << "Sedan váží " << sedan.ziskej_vahu() << " kg.\n";
      nakladni tatra;
      tatra.inicializace(2, 7500.0);
      tatra.inic_nakl(1, 7000.0);
      cout << "Váha tatry je " << tatra.ziskej_vahu() << " kg.\n";
      cout << "Výkonnost tatry je "<<100.0*tatra.vykonost()<<" procent.\n";
      return 0;
    }
    Specifikátor přístupu uvedený před identifikátorem třídy předka určuje přístup ke zděděným složkám třídy. Pokud třída deklaruje svého předka se specifikací public, jsou přístupová práva ke zděděným veřejným a chráněným složkám stejné jako ve třídě předka. Soukromé složky jsou nepřístupné. Deklarujeme-li předka se specifikací protected, omezí se přístupová práva k zděděným veřejným složkám. Složky, které byly v předkovi veřejné nebo chráněné, budou v potomkovi vystupovat jako chráněné a složky, které byly soukromé, zůstávají nepřístupné. Deklarujeme-li předka se specifikací private, budou všechny zděděné veřejné a chráněné složky považovány za soukromé (soukromé složky předka jsou nepřístupné). Pokud u předka nedeklarujeme specifikaci přístupu, platí implicitní specifikace, která je stejná jako implicitní specifikace přístupu k jednotlivým složkám: u třídy typu struct je to specifikace public a u tříd typu class specifikace private.
  3. V další verzi tohoto programu jsou použity konstruktory. Třída vozidlo má konstruktor inicializující položky kola a vaha na implicitní hodnoty. Ostatní dvě třídy mají také konstruktory, inicializující jejich položky na počáteční hodnoty. Jako implicitní hodnoty zde byly zadány neobvyklé hodnoty. V hlavním programu byly odstraněny příkazy provádějící inicializaci (byly změněny na komentář). Program vyzkoušejte.

  4. class vozidlo {
    protected:
      int kola;
      float vaha;
    public:
      vozidlo(void){kola = 7; vaha = -55.7;}
      void inicializace(int nova_kola, float nova_vaha);
      int ziskej_kola(void){return kola;}
      float ziskej_vahu(void){return vaha;}
      float vaha_na_kolo(void){return vaha/kola;}
    };
    class automobil : public vozidlo {
    private:
      int osob;
    public:
      automobil(void) {osob = 12;}
      void inicializace(int nova_kola, float nova_vaha, int lidi = 4);
      int pasazeru(void){return osob;}
    };
    class nakladni : public vozidlo {
    private:
      int osob;
      float naklad;
    public:
      nakladni(void) {osob = 9; naklad = 12.3;}
      void inic_nakl(int kolik = 2, float max_naklad = 12000.0);
      float vykonost(void);
      int pasazeru(void){return osob;}
    };
    void vozidlo::inicializace(int nova_kola, float nova_vaha){
      kola = nova_kola;
      vaha = nova_vaha;
    }
    void automobil::inicializace(int nova_kola, float nova_vaha, int lidi){
      osob = lidi;
      vozidlo::inicializace(nova_kola, nova_vaha);
    }
    void nakladni::inic_nakl(int kolik, float max_naklad){
      osob = kolik;
      naklad = max_naklad;
    }
    float nakladni::vykonost(void){
      return naklad / (naklad + ziskej_vahu());
    }
    int main(int argc, char **argv)
    {
      vozidlo unicykl;
      //   unicykl.inicializace(1, 7.5);
      cout << "Unicykl má " << unicykl.ziskej_kola() << " kol.\n";
      cout << "Zátěž na kolo unicyklu je "<< unicykl.vaha_na_kolo() << endl;
      cout << "Váha unicyklu je " << unicykl.ziskej_vahu() << " kg.\n";
      automobil sedan;
      //   sedan.inicializace(4, 1500.0, 5);
      cout << "Sedan má " << sedan.pasazeru() << " osob.\n";
      cout << "Sedan má zátěž " << sedan.vaha_na_kolo() << " kg na kolo.\n";
      cout << "Sedan váží " << sedan.ziskej_vahu() << " kg.\n";
      nakladni tatra;
      //   tatra.inicializace(2, 7500.0);
      //   tatra.inic_nakl(1, 7000.0);
      cout << "Váha tatry je " << tatra.ziskej_vahu() << " kg.\n";
      cout << "Výkonnost tatry je "<<100.0*tatra.vykonost()<<" procent.\n";
      return 0;
    }
    K zjištění, kdy je který konstruktor automaticky volán přidejte do všech konstruktorů v tomto programu příkazy k výpisu zprávy informující o volání konstruktoru. Prozkoumejte pořadí volání konstruktorů (i u odvozených tříd).
    Pokud neurčíme něco jiného, vyvolá C++ nejprve bezparametrický konstruktor rodičovské třídy a poté začne provádět vlastní konstruktor. Pokud chceme inicializovat zděděné složky jiným způsobem, musíme v hlavičce konstruktoru uvést za dvojtečkou odpovídající volání rodičovského konstruktoru.
  5. Ve skutečných aplikacích obvykle umisťujeme deklaraci a definici třídy do jednoho hlavičkového a jednoho zdrojového souboru. Tyto soubory mají jméno, které odpovídá jménu třídy. Např. pokud máme třídu mojeTrida, pak umístíme její definici do zdrojového souboru MOJETRIDA.CPP a její deklaraci do hlavičkového souboru MOJETRIDA.H (můžeme používat dlouhá jména souborů).

  6. Ukážeme si to na následující konzolové aplikaci. Začneme vývoj nové konzolové aplikace (uložíme ji do souboru LETISTE.CPP). Obsah tohoto souboru je tento:
    #include <condefs.h>
    #include <iostream.h>
    #include <conio.h>
    #pragma hdrstop
    USEUNIT("letadlo.cpp");
    #include "letadlo.h"
    int ziskejVstup(int max);
    void ziskejPrvky(int& rychlost, int& smer, int& vyska);
    int main(int argc, char **argv)
    {
      char vracenaZprava[100];
      // nastavení pole letadel a vytvoření tří objektů letadel
      Letadlo* letadla[3];
      letadla[0] = new Letadlo("TWA 1040");
      letadla[1] = new Letadlo("United Express 749", DOPRAVNI);
      letadla[2] = new Letadlo("Cessna 3238T", SOUKROME);
      // začátek cyklu
      do {
        int letadlo, zprava, rychlost, vyska, smer;
        rychlost = vyska = smer = -1;
        cout << endl << "Kterému letadlu chcete zaslat zprávu?";
        cout << endl << endl << "0. Konec" << endl;
        for (int i = 0; i < 3; i++)
          cout << (i + 1) << ". " << letadla[i]->jmeno << endl;
        // Získání čísla letadla
        letadlo = ziskejVstup(4);
        // Při 0 konec cyklu
        if (letadlo == -1) break;
        cout << endl<<"Vybráno "<<letadla[letadlo]->jmeno<<endl<<endl;
        cout << "Jakou zprávu chcete zaslat?" << endl;
        cout << endl << "0. Konec" << endl;
        cout << "1. Změna stavu" << endl;
        cout << "2. Vzlet" << endl;
        cout << "3. Přistání" << endl;
        cout << "4. Zpráva o stavu" << endl;
        zprava = ziskejVstup(5);
        if (zprava == -1) break;
        if (zprava == 0) ziskejPrvky(rychlost, smer, vyska);
        bool dobraZprava = letadla[letadlo]-> PrijmiZpravu(
          zprava, vracenaZprava, rychlost, smer, vyska);
        if (!dobraZprava) cout << endl << "Nelze provést.";
        cout << endl << vracenaZprava << endl;
      } while (1);
      for (int i = 0; i < 3; i++) delete letadla[i];
      return 0;
    }
    int ziskejVstup(int max)
    {
      int volba;
      do {
        volba = getch();
        volba -= 49;
      } while (volba < -1 || volba > max);
      return volba;
    }
    void ziskejPrvky(int& rychlost, int& smer, int& vyska)
    {
      cout << endl << "Zadej novou rychlost: ";
      cin >> rychlost;
      cout << "Zadej nový směr: ";
      cin >> smer;
      cout << "Zadej novou výšku: ";
      cin >> vyska;
      cout << endl;
    }
    Dále zvolíme File | New | Text a vytvoříme hlavičkový soubor třídy LETADLO.H (uložíme jej do stejného adresáře jako LETISTE.CPP). Soubor bude mít tento obsah:
    #ifndef letadloH
    #define letadloH
    #define LINKOVE     0
    #define DOPRAVNI    1
    #define SOUKROME    2
    #define VZLET       0
    #define LET         1
    #define PRISTANI    2
    #define NADRAZE     3
    #define ZPR_ZMENA   0
    #define ZPR_VZLET   1
    #define ZPR_PRIST   2
    #define ZPR_ZPRAVA  3
    class Letadlo {
      public:
        Letadlo(const char* _jmeno, int _typ = LINKOVE);
        ~Letadlo();
        virtual int ziskejStav(char* stavovyRetezec);
        int ziskejStav() {return stav; }
        int Rychlost() { return rychlost; }
        int Smer() { return smer; }
        int Vyska() { return vyska; }
        void ZpravaStav();
        bool PrijmiZpravu(int zpr, char* odpoved, int rych = -1,
                         int sme = -1, int vys = -1);
        char* jmeno;
      protected:
        virtual void Vzletni(int smer);
        virtual void Pristan();
      private:
        int rychlost;
        int vyska;
        int smer;
        int stav;
        int typ;
        int maxVyska;
    };
    #endif
    Obdobně vytvoříme další soubor LETADLO.CPP (uložíme jej opět do stejného adresáře) s obsahem:
    #include <stdio.h>
    #include <iostream.h>
    #include "letadlo.h"
    Letadlo::Letadlo(const char* _jmeno, int _typ):
      typ(_typ), stav(NADRAZE), rychlost(0), vyska(0), smer(0)
    {
      switch (typ) {
        case LINKOVE : maxVyska = 10000; break;
        case DOPRAVNI : maxVyska = 6000; break;
        case SOUKROME : maxVyska = 3000; break;
      }
      jmeno = new char[50];
      strcpy(jmeno, _jmeno);
    }
    Letadlo::~Letadlo()
    {
      delete[] jmeno;
    }
    bool Letadlo::PrijmiZpravu(int zpr, char* odpoved, int rych,
                         int sme, int vys)
    {
      if (rych > 800) {
        strcpy(odpoved, "Rychlost nemůže být větší než 800.");
        return false;
      }
      if (sme > 360) {
        strcpy(odpoved, "Směr nemůže být větší než 360 stupnů.");
        return false;
      }
      if (vys < 100 && vys != -1) {
        strcpy(odpoved, "Jsme příliš nízko.");
        return false;
      }
      if (vys > maxVyska) {
        strcpy(odpoved, "To je příliš vysoko.");
        return false;
      }
      switch (zpr) {
        case ZPR_VZLET : {
          if (stav != NADRAZE) {
            strcpy(odpoved, "Jsem již ve vzduchu.");
            return false;
          }
          Vzletni(sme);
          break;
        }
        case ZPR_ZMENA : {
          if (stav == NADRAZE) {
            strcpy(odpoved, "Jsem na zemi.");
            return false;
          }
          if (rych != -1) rychlost = rych;
          if (sme != -1) smer = sme;
          if (vys != -1) vyska = vys;
          stav = LET;
          break;
        }
        case ZPR_PRIST : {
          if (stav == NADRAZE) {
            strcpy(odpoved, "Jsme na zemi.");
            return false;
          }
          Pristan();
          break;
        }
        case ZPR_ZPRAVA : ZpravaStav();
      }
      strcpy(odpoved, "Provedeno.");
      return true;
    }
    void Letadlo::Vzletni(int sme)
    {
      smer = sme;
      stav = VZLET;
    }
    void Letadlo::Pristan()
    {
      rychlost = smer = vyska = 0;
      stav = NADRAZE;
    }
    int Letadlo:: ziskejStav(char* stavovyRetezec)
    {
      sprintf(stavovyRetezec, "%s, Výška: %d, Směr: %d, Rychlost: %d\n",
              jmeno, vyska, smer, rychlost);
      return stav;
    }
    void Letadlo::ZpravaStav()
    {
      char buff[100];
      ziskejStav(buff);
      cout << endl << buff << endl;
    }
    Když se podíváme na hlavičkový soubor třídy Letadlo, pak na jeho začátku vidíme řadu direktiv #define. Definujeme zde makra, která nahradí textové řetězce číselnými hodnotami (řetězce si zapamatujeme snadněji než čísla). Posuďte sami, který z následujících příkazů je srozumitelnější?
    if (typ == LINKOVY) ...
    // nebo
    if (typ == 0) ...
    Jména těchto konstant obvykle zapisujeme velkými písmeny (pro snadné odlišení od proměnných). Jiným způsobem deklarace konstant je deklarace proměnné s modifikátorem const. Např.
    const int LINKOVY = 0;
    Použití konstantní proměnné je modernější metoda než definice konstant pomocí maker.
    Další řádky hlavičkového souboru obsahují deklaraci třídy. Krátké metody jsou zde deklarovány jako vložené funkce. Je zde také překrytá funkce ziskejStav. Povšimněte si, že ve třídě je pouze jedna veřejná datová složka. Ostatní jsou soukromé a jsou tedy dostupné pouze pomocí metod. Tzn. pokud požadujeme změnu rychlosti, výšky nebo směru, pak musíme instanci Letadlo zaslat zprávu. To odpovídá skutečnosti. Řídící letového provozu také nemůže fyzicky změnit směr letadla. Může pouze zaslat zprávu pilotovi a ten požadovanou změnu provede.
    Nyní přejdeme k definičnímu souboru třídy. Konstruktor provádí inicializaci, včetně dynamické alokace místa pro pole znaků k uložení jména letadla. Tato paměť je uvolňována v destruktoru. Většinu práce provádí metoda PrijmiZpravu. Příkaz switch určuje, která zpráva byla přijata a je provedena příslušná akce. Povšimněte si, že metody Vzletni a Pristan nemohou být volány přímo (jsou chráněné), ale prostřednictvím PrijmiZpravu. Nelze tedy říci letadlu aby vzlétlo nebo přistálo, ale lze mu zaslat zprávu, aby to provedlo. Metoda ZpravaStav volá ZiskejStav k získání stavového řetězce, který potom metoda vypíše.
    Hlavní program deklaruje pole ukazatelů na Letadlo a vytváří tři instance této třídy. Dále začíná cyklus ve kterém zasíláme zprávy objektům Letadlo voláním funkce PrijmiZpravu. Po odeslání zprávy, čekáme na odpověď od letadla. Tento cyklus poběží stále, dokud není ukončen příkazem break.
    Rozdělovat program do několika souborů je vhodné u složitějších aplikací. U našich (zatím jednoduchých) aplikací zůstaneme u jednoho zdrojového souboru.

Kontrolní otázky:

  1. Jak můžeme dosáhnou toho, že metody budou nedotupné z vnějšku třídy a budeme je moci volat v odvozených třídách?
  2. Co je to objekt?
  3. Může mít třída více než jeden konstruktor?
  4. Jak se liší třída od struktury v C++?
  5. Jaký význam mají soukromé datové složky?
  6. Jak můžeme u soukromých datových složek umožmit uživateli číst a nastavovat jejich hodnoty?
  7. Jak a kdy je volán destruktor třídy?
  8. K čemu slouží inicializační seznam třídy?
  9. Může třída obsahovat instance jiné třídy jako datové složky?
Řešení


4. Základy OOP IV