2. Základy OOP II
  1. Ve třídě může být definováno několik konstruktorů (liší se počtem a typy parametrů). Výjimečné postavení mají bezparametrický konstruktor a kopírovací konstruktor. Bezparametrický konstruktor je konstruktor bez parametrů (nebo konstruktor jehož všechny parametry mají definované implicitní hodnoty). Implicitní verze bezparametrického konstruktoru (konstruktoru vytvořeného překladačem), pouze vyhradí místo pro definovanou instanci. Kopírovací konstruktor je konstruktor, jehož jediným parametrem je odkaz na proměnnou téhož typu, jakého je tento konstruktor. Také kopírovací konstruktor umí v případě potřeby vytvořit překladač. Tato implicitní verze kopírovacího konstruktoru, vyhradí potřebnou paměť pro nový objekt a zkopíruje obsah svého parametru do vytvářeného objektu.

  2. Následují zásady používání konstruktorů: Konstruktory se volají ve chvíli, kdy jsou objekty vytvářeny. Definujeme-li globální nebo statický objekt, provede se konstruktor ještě před inicializačními funkcemi (před funkcemi uvedenými v direktivách #pragma startup, a tím samozřejmě i před funkcí main; inicializační funkce mohou počítat s tím, že všechny statické objekty jsou již připraveny k použití). Automatické objekty (objekty, které nejsou globální ani statické) jsou vytvářeny při každém vstupu do programu a stejně tak jsou volány i jejich konstruktory.
    Toto si nyní ukážeme na příkladě. Máme následující třídy:
    class A {
    public:
      int a1;
      int a2;
      void metoda() {};
    };
    class B {
    public:
      int bi;
      A bA;
      static int pocet;             // počet vytvořených instancí
      B(A& a, int i = 0){           // definice konstruktoru
        bA = a;
        bi = i;
        pocet++;
      };// Je-li definován libovolný konstruktor, pak překladač negeneruje bezparametrický konstruktor
        // (v případě potřeby jej musíme definovat sami, např. jako B() {}; nebo jako následující konstruktor)
      B(int = 0, int = 0, int =0);
      B(B& b);   // kopírovací konstruktor
    };
    int B::pocet = 0;
    inline B::B(int i1, int i2, int i3){
      bi = i1;
      bA.a1 = i2;
      bA.a2 = i3;
      pocet++;
    }
    B::B(B& b){
      bi = b.bi;
      bA.a1 = b.bA.a1;
      bA.a2 = b.bA.a2;
      pocet++;
    }
    Pro tyto třídy lze používat konstruktory takto:
    A as1;             // použije se implicitní bezparametrický konstruktor
    A as2 = {0, 2};// způsob používaný při inicializaci strukturovaných datových typů
    const A as3 = as2; // použije se implicitní kopírovací konstruktor
    B bs1;             // použije se B(0, 0, 0)
    B bs2 = 22;        // použije se B(int, 0, 0)
    const B bs3 = bs2.bi;      // použije se B(int, 0, 0)
    B bs4 = bs2;       // použije se kopírovací konstruktor B(B&)
    B bs5 = as2;       // použije se B(A&, 0)
    B bs6(22);         // použije se B(int, 0, 0)
    B bs7(bs2.bA.a1);  // použije se B(int, 0, 0)
    B bs8(bs2);        // použije se kopírovací konstruktor B(B&)
    const B bs9(as2);  // použije se B(A&, 0)
    B bs10(as2, 3);    // použije se B(A&, 3)
    B bs11(10, 20, 30);// použije se B(int, int, int)
    B bs12[3];         // použije se 3x B(0, 0, 0)
    Pokuste se pochopit, jak tyto konstruktory pracují a kdy se který z možných konstruktorů použije.
  3. Třída typu union nesmí obsahovat složky, která mají konstruktory, ale sama tato třída konstruktory mít může. Vyzkoušejte (pokuste se deklarovat třídu typu union a jako položky v ní použijte třídy A a B z předchozího zadání).
  4. Dalším typem implicitně volaných konstruktorů jsou konverzní konstruktory. Jsou to konstruktory, které lze volat pouze s jedním parametrem. Použijí se, když potřebujeme inicializovat instanci daného objektového typu a máme k dispozici instanci typu, pro nějž je definován potřebný konverzní konstruktor. Typickým příkladem je např. předávání parametrů funkcím. Jedná se o ekvivalent implicitního přetypování. Uveďme si příklad (předpokládáme platnost předchozích definic):

  5. class C{
    public:
      B b;
      C(int i=0, int j=0, int k=0){b = B(i, j, k);};
      void c(B& bb){b = bb;};            // toto není konstruktor
    };
    B Pokus(B b) {                       // obyčejná funkce
      return b;         // při předávání parametrů hodnotou a při předávání
    }                                                                         // vrácené hodnoty se volá kopírovací konstruktor
    void main(void){
      C c1(10, 20, 30);
      c1.c(B(as2));        // volání B::B(A&) pro přetypování parametru
      for (int i=0; ++i <=3;) bs12[i] = B(i, i*i);
      bs1 = Pokus(bs12[2]);// protože se parametr předává hodnotou, musí se
      // vytvořit nová instance - automaticky se volá kopírovací konstruktor
      bs2 = Pokus(123);// automaticky se volá konverzní konstruktor B::B(int, 0, 0)
    }
    Pokuste se pochopit, kdy je volán který konstruktor.
  6. Objektové složky složených tříd můžeme inicializovat přímo. V hlavičce konstruktoru složené třídy lze určit konstruktor, který chceme použít pro vytvoření dané složky. Provedeme to tak, že za seznamem parametrů konstruktoru složené třídy napíšeme dvojtečku a za ní uvedeme seznam identifikátorů složek, které chceme vytvářet nestandardně. Při vytváření instance se její jednotlivé složky vytvářejí v tom pořadí, v jakém byly deklarovány v definici třídy. Pořadí uvedení inicializátorů složek v hlavičce konstruktoru nemá na pořadí jejich inicializace žádný vliv (v další ukázce jsou schválně přeházeny). Složky, jejichž inicializátory nebudou uvedeny v hlavičce konstruktoru, budou vytvořeny bezparametrickým konstruktorem. Složky, které již byly inicializovány, je možné použít jako parametry v inicializátorech dalších složek.

  7. class D{
    public:
      int i;
      A a;
      B b1;
      B b2;
      C c;
      D(B bb, A aa, int id2, int id3) : i(842), c(i/2, id2, id3), a(aa), b1(bb) {};
    };//složka b2 není v seznamu a bude tedy inicializována bezparametrickým konstruktorem
    D d(bs5, as2, 246, 987);
    Pole objektových typů můžeme inicializovat stejně jako pole instancí neobjektových typů. Jediný rozdíl je v tom, že když není typ inicializační hodnoty mezi typy, pro něž je definován konverzní konstruktor nebo když potřebujeme přiřadit danému prvku pole hodnotu, pro jejíž vytvoření je potřebný konstruktor s více parametry, uvedeme místo dané hodnoty přímo tento konstruktor (viz třetí až šestý prvek pole v následující ukázce). Stejně jako u klasických polí platí, že inicializačních hodnot nesmí být více než je prvků pole a stejně jako u klasických polí nemusíme inicializovat všechny prvky daného pole (prvky na než se inicializační hodnoty nedostanou budou inicializovány bezparametrickým konstruktorem).
    B bpole[9] = {as2, 124, bs2, B(321, 765), B(as2, 654), B(bs7), B(1, 2, 3)};
    Pokuste se pochopit jak inicializace objektových typů probíhá.
  8. Můžeme mít také datovou složku třídy, která je odkazem. V tomto případě odkaz může být inicializován pouze v inicializačním seznamu třídy a nikde jinde:

  9. class mojeTrida {
      jinaTrida& jina;        // odkaz na jinou třídu
      // ...
    public:
      mojeTrida();
      // ...
    };
    mojeTrida::mojeTrida() :
      jina(*new jinaTrida)    // musí být umístěno zde
    {
    }
  10. Složky, které jsou deklarovány se specifikátorem const, jsou konstantní složky. Konstantní složky uchovávají hodnoty, které se nastaví při vzniku instance a v průběhu jejího života se již nemění. Může to být např. čas vzniku dané instance, její pořadové číslo nebo jiný údaj, který nebude potřeba měnit. Vytvořte jednoduchou třídu s konstantní složkou por_cis (pořadové číslo instance) a vyzkoušejte jak pracuje (v konstruktoru toto pořadové číslo vypisujte, vytvořte několik objektů a u některého z nich se pokuste tuto složku změnit).
  11. Napište deklaraci a implementaci třídy datum se soukromými položkami den, mesic a rok, konstruktorem s implicitními parametry (s nulovou hodnotou) a metodami nastav (nastavení všech položek), nastavDen, nastavMesic, nastavRok, Den (zjištění dne), Mesic a Rok. U konstruktoru a všech nastavovacích metod zajistěte, v případě nevhodné hodnoty některého parametru, použití hodnoty ze systémového datumu (např. pomocí standardní funkce getdate). Toto lze využít v případě, kdy použijete bezparametrický konstruktor, pak hodnoty budou nastaveny na systémový datum. Tuto třídu ještě doplňte přetížením operátoru << (výstup datumu do datového proudu). V následující implementaci tohoto operátoru je uvedeno mnoho komentářů, aby jste se naučili definovat takovéto operátory i pro své vlastní datové typy.

  12. ostream& operator<< (ostream& o, datum& D){
      //Zapamatuj si původní nastavení formátovacích příznaků a nastav výpis v des.
      //soustavě zarovnávaný doprava. Hodnota ostatních příznaků není významná.
      long flags = o.flags(ios::right + ios::dec);
      //zjisti rozdíl mezi požadovanou a potřebnou velikostí výpisové oblasti
      int width = o.width() - 8;
      //zapamatuj si nastavený výplňový znak, my použijeme nulu, např. 01.03.99
      char fill = o.fill('0');
      //pokud je požadovaná oblast větší a pokud nebylo nastaveno zarovnávání vlevo
      //zaplň úvodní přebytek původními výplňovými znaky
      if ((width > 0) && !(flags & ios::left)){
      //proměnná i z následujícího cyklu je v tomto bloku lokální
        for (int i = 0; i < width; i++, o << fill);
      }
      //výpis datumu
      o << setw(2) << D.Den() << "." << setw(2) << D.Mesic() << "."
        << setw(2) << (D.Rok()%100);
      //obnova původního nastavení zaplnění závěrečného přebytku výplňovými znaky
      o.fill(fill);
      o.flags(flags);
      if ((width > 0) && (flags & ios::left)){
        for (int i = 0; i < width; i++, o << fill);
      }
      return o;
    }
    Vytvořenou třídu vyzkoušejte v nějakém programu.
  13. Stejně jako datové složky třídy mohou být se specifikátorem static, mohou být s tímto specifikátorem i metody třídy. Definují se stejně jako běžné metody, pouze musíme mít na paměti, že nejsou svázány s žádnou instancí (jedná se vlastně o funkci) a nelze v nich tedy používat klíčové slovo this. Tyto metody můžeme kvalifikovat stejně jako složky třídy se specifikátorem static, buď prostřednictvím identifikátoru nějaké instance (ten oddělujeme tečkou) nebo prostřednictvím identifikátoru třídy (oddělujeme jej dvěma dvojtečkami). Metody se specifikátorem static se používají když potřebujeme pracovat pouze se složkami static, když chceme omezit přístupová práva k dané funkci na metody dané třídy nebo když potřebujeme nějakou funkci, která nemůže být běžnou metodou, aby přistupovala k soukromým složkám třídy. Pokuste se vytvořit nějakou třídu, ve které použijete metodu se specifikátorem static a tuto metodu vyzkoušejte.
  14. Přáteli se mohou stávat jak jednotlivé funkce, tak i celé třídy. Pokud je přítelem deklarované třídy celá třída, vztahuje se možnost přístupu ke všem složkám deklarované třídy na všechny metody spřátelené třídy. Nositelem sdělení o přátelství může být jedině třída o jejichž soukromých položkách se jedná. Přátelé se označují pomocí klíčového slova friend, které je následováno buď prototypem funkce nebo identifikátorem třídy, kterou právě deklarovaná třída označuje za svého přítele. Pokud chceme zdůraznit, že deklarovaným přítelem je třída, můžeme před identifikátor spřátelené třídy uvést klíčové slovo class, struct nebo union. Např.

  15. class tajna_schranka{
      char *obsah;
      tajna_schranka(char *o = "") {obsah = o;};
      friend class agent;
    };
    V tomto případě jsou datová položka a konstruktor soukromé a lze k nim přistupovat pouze ze samotné třídy tajna_schranka a ze spřátelené třídy agent. Vyzkoušejte.
  16. Můžeme definovat i konstanty objektových datových typů. Má to ale jeden háček: překladač neumí v obecném případě posoudit, zda nějaká metoda ovlivní hodnotu své instance či nikoliv a tudíž nám pro jistotu nedovolí pro konstantu použít žádnou metodu. Jediné, co s ní můžeme dělat, je předávat ji jako parametr funkcím, které mají ve svém prototypu u příslušného parametru specifikátor const. Aby bylo možno aplikovat na konstantní objekty metody, můžeme označit metody, které nemění hodnoty instance, pro kterou je voláme, takže je lze bezpečně použít i na konstanty. Tyto metody se v deklaraci (v definici i v prototypu) označují klíčovým slovem const, které se uvede za závorkou, ukončující seznam parametrů. Např.

  17. class datum {
      int den;
      int mesic;
      int rok;
    public:
      int Den() const {return den;};
      int Mesic() const {return mesic;};
      int Rok() const;
    };
    int datum::Rok() const{return rok;}
    Vytvořte nějaké konstantní instance a vyzkoušejte.
2. Základy OOP II