8. Základy OOP VIII
  1. Obdobně jako binární operátory můžeme přetěžovat i unární operátory (! ~ + -). Pamatujme si pouze, že pokud chceme definovat homonyma těchto operátorů jako řadové funkce, musíme je definovat jako funkce s jedním parametrem (musí být objektového nebo výčtového typu). Pokud je definujeme jako metody, pak budou bez parametrů (operand bude instancí, pro kterou tuto metodu voláme). Definujte ve třídě retezce homonyma unárních operátorů + a -, která budou převádět řetězce na velká resp. malá písmena.
  2. V následující ukázce je uvedeno přetížení operátorů inkrementace a dekrementace (++ a --). Uvědomte si, že tyto operátory existují jako prefixové a postfixové. Prostudujte si přetěžování těchto operátorů.

  3. class ID {
      int i;
      double d;
    public:
      ID() {i = 1; d = 1.1; }
      ID& operator++(){i++; return *this;}
      ID operator++(int){ID id = *this; d+=0.1; return id;}
      friend ID& operator--(ID& x){x.i--; return x;}
      friend ID operator-- (ID& x, int){ID id = x; x.d-=0.1; return id;}
      friend ostream& operator<< (ostream& o, ID x){
        o << "[" << x.i << "; " << x.d << " ]" ;
        return o;
      }
    };
    int main(int argc, char **argv)
    {
      ID a;
      cout << "Počátek: " << a << endl;
      cout << "Preinkrement: " << ++a;
      cout << " - Výsledek " << a << endl;
      cout << "Postinkrement: " << a++;
      cout << " - Výsledek " << a << endl;
      cout << "Predekrement: " << --a;
      cout << " - Výsledek " << a << endl;
      cout << "Postdekrement: " << a--;
      cout << " - Výsledek " << a << endl;
      return 0;
    }
    Chceme-li přetížit prefixový operátor ++ nebo --, musíme jej definovat buď jako metodu bez parametrů, nebo jako běžnou funkci s jedním parametrem objektového nebo výčtového typu. Chceme přetížit postfixový operátor musíme jej deklarovat buď jako metodu s jedním parametrem typu int, nebo jako běžnou funkci se dvěma parametry, z nichž první je objektového nebo výčtového typu a druhý je typu int.
    Povšimněte si opět použití anonymních parametrů (celočíselný parametr v postfixových operátorech). Dále si povšimněte jak donutíme operátor, aby se choval postfixově (původní hodnotu nejprve odložíme, provedeme činnost operátoru a nakonec odloženou hodnotu vrátíme. V souvislosti s prefixovým a postfixovým chováním homonym musíme myslet i na typ vrácené hodnoty. U prefixových operátorů je v podstatě jedno, zda budeme vracet hodnotu, odkaz či ukazatel. Pokud operátor přebírá parametr odkazem ví, že daný parametr existoval již před voláním operátoru, a může proto vrátit odkaz na něj. Postfixové operátory nemají na vybranou, musí výsledek vrátit hodnotou.
  4. V dalším příkladu si ukážeme překrytí operátoru indexování ([]). Jednou z nejčastějších výhrad proti jazyku C je jeho neschopnost kontrolovat překročení mezí při práci s poli. C++ přináší prostředky, kterými je možno podobné problémy řešit poměrně elegantně (viz následující příklad):

  5. #define LADENI 1                          //po odladění změníme 1 na 0
    class pole {
      const int n;                                  //počet prvků pole
      int *const p;                                 //ukazatel na začátek pole
    public:
      pole(int Mez) : n(Mez), p(new int[Mez]){};         //konstruktor vytvoří pole
      ~pole() {delete [] p;}
      int& operator[] (int);                                           //homonymum operátoru indexace
      int prvku() {return n;}                                         //dotaz na počet prvků pole
    };
    #if LADENI                                      //větec aktivovaná pro fázi ladění
    int& pole::operator[] (int i) {
      assert((i>=0) && (i < n));
      return p[i];
    }
    #else                                               //větev aktivovaná po odladění
    int& pole::operator[] (int i) {
      return p[i];
    }
    #endif
    int main(int argc, char **argv)
    {
      pole P(2);
      for (int s=0, i=0; i <= P.prvku(); i++){              //při posledním průchodu je překročena mez pole
        P[i] = i;
        s += P[i];
      }
      return 0;
    }
    V tomto příkladě je použito makro assert (je v hlavičkovém souboru assert.h), jehož úkolem je otestovat podmínku, kterou mu předáme jako parametr. V případě, že tato podmínka není splněna, ukončí se běh programu a do standardního chybového výstupu se zapíše zpráva o chybě. Tato zpráva bude obsahovat jméno souboru a číslo řádku, na němž se makro nachází a testovanou podmínku. Okno konzolové aplikace je uzavřeno a my žádnou informaci nevidíme. Po odladění programu změníme první řádek programu a díky podmíněnému překladu bude makro z programu vypuštěno.
    Při přetěžování operátoru indexování nesmíme zapomenout, že tento operátor smíme přetížit pouze jako nestatickou metodu objektového typu s jedním parametrem. Možnost přetěžovat tento operátor před námi otevírá netušené perspektivy, např. Pokuste se pochopit práci předchozího programu.
  6. Pokusíme se vyřešit problém uložení symetrické matice v paměti, tj. uložení pouze poloviny takovéto matice. Problém vyřešíme pomocí přetížení operátoru volání funkce. Tento operátor nám umožňuje používat instanci daného objektového typu jako funkci. Operátory volání funkce mají oproti ostatním operátorům jednu zvláštnost: nemají přesně definovaný počet operandů, takže si jejich homonyma můžeme zcela přizpůsobit svým potřebám. V následující ukázce definujeme třídu symetrických matic smat. V ní přetížíme operátor funkčního volání s dvěma celočíselnými parametry. Tento operátor bude zastupovat maticový operátor indexování. Pro zjednodušení ukázky (abychom nemuseli definovat další metody) jsou zde všechny složky třídy deklarovány jako veřejné.

  7. class smat {
    public:
      int N;
      int NN;
      int *P;
      smat(int n);
      ~smat(){delete [] P;}
      int& operator()(int i, int j);
    };
    smat::smat(int n): N(n), NN(n*(n+1)/2), P(new int[NN]){
      int i, j;
      for (i=0; i<N; i++)
      for (j=0; j<N; j++)
      (*this)(i,j) = i*10+j;
    }
    int& smat::operator()(int i, int j){
      if (i<j) {int p=i; i=j; j=p;}
      return (this->P[(i*(i+1))/2+j]);
    }
    int main(int argc, char **argv)
    {
      smat M(4);
      int i;
      for (i=0; i<4; i++) M(i,i) += 100 * M(i, i);
      for (i=0; i<M.NN; i++) cout << M.P[i] << ", ";
      return 0;
    }
    Pokuste se určit, jak tento příklad pracuje.
  8. Jazyk C++ umožňuje definovat jako metodu objektového typu funkci, která bude provádět přetypování. Jméno této metody je tvořeno klíčovým slovem operator, za kterým uvedeme identifikátor cílového typu. Operátor přetypování nemá parametry a v jeho deklaraci neuvádíme typ vrácené hodnoty, neboť ten je určen již jménem této funkce. Např.

  9. operator int(); nebo operator void *();
    Podívejte se na následující program:
    class zlomek{
      long Cit, Jm;
    public:
      zlomek(long C=0, long J=1) {Cit = C; Jm = J;}
      operator double() {return (double(Cit) / Jm);}
      zlomek operator * (zlomek& Z) {return zlomek(Cit*Z.Cit, Jm*Z.Jm);}
      friend ostream& operator<< (ostream &o, zlomek &z);
    };
    ostream& operator<< (ostream &o, zlomek &z){
      o << z.Cit << "/" << z.Jm;
      return o;
    }
    int main(int argc, char **argv)
    {
      zlomek Z2(1,2);
      zlomek Z3 = 3;
      cout << "Z2 = " << Z2 << endl;
      cout << "Z3 = " << Z3 << endl;
      cout << "Z2 * Z3 = " << Z2 * Z3 << endl;
      cout << "Z2 * Z3 = " << double(Z2 * Z3) << endl;
      return 0;
    }
    V tomto programu pracujeme se třídou zlomek. V této třídě je definován operátor přetypování na typ double (je použit v posledním příkazu pro převod zlomku na reálné číslo). Vyzkoušejte, co se stane, když v tomto programu nebude použito přetížení operátoru <<. Zdůvodněte dosažený výsledek.
  10. V nových verzích C++ je možno v definici třídy definovat nejen datové složky a metody, ale i nové vnořené (a tedy lokální) datové typy. Vnořovat přitom můžeme jak typy neobjektové, tak i typy objektové. S vnořením neobjektových datových typů se můžeme setkat např. v definici datových proudů, které jsou součástí standardní knihovny. Vnoření datových typů používáme především proto, abychom předem zamezili případné možné záměně s dalšími identifikátory, resp. abychom neblokovali některé identifikátory pro pozdější použití. V takovém případě definujeme vnořený datový typ jako veřejný. Pokud chceme definovat datový typ určený pouze pro danou třídu a její přátele, definujeme jej jako soukromý. Pokud se budeme chtít na datový typ odvolávat, musíme tak provést plnou kvalifikací (identifikátor třídy, dvě dvojtečky a jméno typu). V souboru IOSTREAM.H v definici třídy ios najdeme např. následující vnořené datové typy:

  11. class ios {
    public:                        // stavové bity proudů
      enum io_state   {
           goodbit  = 0x00,        // vše OK
           eofbit   = 0x01,        // byl nalezen konec souboru
           failbit  = 0x02,        // poslední operace byla neúspěšná
           badbit   = 0x04,        // pokus o nedovolenou operaci
           hardfail = 0x80         // blíže nespecifikovaná chyba
      };
      enum open_mode  {       // režim proudů - při otevírání
           in   = 0x01,       // otevřít pro čtení
           out  = 0x02,       // otevřít pro zápis
           ate  = 0x04,       // po otevření přesun na konec souboru
           app  = 0x08,       // přidávej pouze na konec souboru
           trunc    = 0x10,   // existuje-li soubor, vyčisti jej
           nocreate = 0x20,   // nesmí se vytvořit nový soubor
           noreplace= 0x40,   // nesmí se přepsat existující soubor
           binary   = 0x80    // binární soubor
      };
      enum seek_dir { beg=0, cur=1, end=2 }; // referenční bod pro přesuny
      enum    {                     // formátovací příznaky
            skipws    = 0x0001,     // přeskakuj na vstupu bílé znaky
            left      = 0x0002,     // zarovnávej výstup vlevo
            right     = 0x0004,     // zarovnávej výstup vpravo
            internal  = 0x0008,     // zarovnávej oboustraně
            dec   = 0x0010,         // desítková soustava
            oct   = 0x0020,         // osmičková soustava
            hex   = 0x0040,         // šestnáctková soustava
            showbase  = 0x0080,     // označ soustavu vystupujících čísel
            showpoint = 0x0100,     // zobrazuj desetinnou tečku
            uppercase = 0x0200,     // šestnáctkové číslice velkými písmeny
            showpos   = 0x0400,     // zobraz + u kladných čísel
            scientific= 0x0800,     // semilogaritmický tvar
            fixed     = 0x1000,     // běžný tvar reálných čísel
            unitbuf   = 0x2000,     // po zápisu spláchni všechny proudy
            stdio     = 0x4000      // po zápisu spláchni stdout a stderr
      };
     .....
    Poslední z vnořených výčtových datových typů nemá dokonce ani jméno, takže nemůžeme definovat žádné jeho instance. Slouží pouze k pojmenování formátovacích příznaků.
    V této kapitole si zopakujeme a rozšíříme informace o datových proudech. Datové proudy C++ jsou založeny na dvou hierarchiích objektových typů. Jednodušší z nich je odvozena od třídy strembuf a obsahuje objekty, které tvoří vyrovnávací paměti pro datové proudy. Potomci třídy streambuf obsahují metody specifické pro proudy orientované na soubory, řetězce a konzolu. Programátor obvykle o těchto třídách nepotřebuje vědět více, než že existují. Pracují s nimi metody třídy ios a jejich potomků, které programátor opravdu využívá. Hierarchie odvozená od ios je podstatně rozvětvenější.
    Třída ios je virtuálním předkem dalších tříd a je definována v iostream.h. Položky ios jsou chráněné a jsou tedy přístupné i v odvozených třídách. Třída ios obsahuje ukazatel na sdružený objekt typu strembuf, tedy na přidruženou vyrovnávací paměť. ios dále obsahuje položku state typu int, která obsahuje příznaky možných chybových stavů proudu (tedy vlastně příznaky toho, zda se poslední vstupní nebo výstupní operace s tímto proudem podařila nebo k jaké chybě došlo). Tyto příznaky popisuje veřejně přístupný výčtový typ ios::io_state (viz výše). Položka x_flags je typu long. Obsahuje formátovací příznaky. Ty jsou popsány pomocí nepojmenovaného veřejně přístupného výčtového typu deklarovaného taktéž ve třídě ios. Další tři položky x_precision, x_width a x_fill jsou typu int a obsahují přesnost (počet zobrazovaných desetinných míst), šířku výstupního pole a výplňový znak. Implicitní hodnota prvních dvou je 0, u posledního je to mezera. Většina metod třídy ios nastavuje nebo vrací hodnoty položek (a tak zjišťuje stav proudu nebo určuje formátování). Hodnoty jednotlivých stavových bitů v položce state lze zjišťovat pomocí metod bad, eof, fail a good, které vracejí hodnoty typu int. Např. příkaz
    if (cout.bad()) Konec();
    způsobí volání funkce Konec, jestliže v proudu cout došlo k závažné chybě (je nastaven příznak badbit v io_state). Voláním metody ios::clear lze nastavit nebo vynulovat příznaky chyb (kromě příznaku hardfail). Metoda ios::rdstate vrací slovo obsahující všechny chybové příznaky daného proudu (tedy položku state). Metoda long ios::flags vrátí hodnotu formátovacích příznaků, uložených v položce x_flags. Metoda long ios::flags(long priznaky) vrátí původní hodnotu příznaků a nastaví nové, dané jednotlivými bity parametru priznaky. Metody int ios::precision() a int ios::precision(int P) vracejí přesnost; druhá z nich také nastavuje novou hodnotu přesnosti. Metody int ios::width() a int ios::width(int s) vracejí nastavenou šířku vstupního nebo výstupního pole; druhá z nich tuto šířku také nastavuje. Metoda long ios::setf(long priznaky) vratí předchozí nastavení formátovacích příznaků a nastaví novou hodnotu, danou parametrem. Metoda char ios::fill(char vypln) vrátí předchozí vyplňovací znak a nastaví nový, daný parametrem, zatímco char ios::fill() vrátí pouze původní vyplňovací znak. Pro testování stavu datových proudů se často využívají přetížené operátory ! a (void*). Operátor ! vrací 1, jestliže se poslední vstupní nebo výstupní operace s proudem nepodařila. Operátor (void*) vrací nulu, pokud se poslední operace nepodařila a ukazatel na proud, jestliže proběhla v pořádku. Vrácený ukazatel nelze dereferencovat.
    Od třídy ios jsou odvozeny třídy istream a ostream, které představují základ vstupních a výstupních datových proudů, třída fstreambase, která je základem proudů, orientovaných na soubory a třída strstreambase, která je základem paměťových proudů. Také tyto třídy se v programech přímo nepoužívají. Od nich jsou pak odvozeny třídy fstream, strstream, istream_withassign, ostream_withassign a některé další, které již opravdu slouží ke vstupním a výstupním operacím. Pokud výslovně neuvedeme něco jiného, jsou tyto třídy deklarovány v iostream.h.
    Třída istream je základem vstupních proudů. V této třídě je pro účely formátovaného vstupu přetížen operátor >>. Deklarace tohoto operátoru pro typ int má tvar
    istream& istream::operator>> (int&);
    Tento operátor vrací odkaz na datový proud, pro který jej zavoláme. To znamená, že např. výraz
    cin >> i;
    představuje odkaz na proud cin. Díky tomu můžeme přetížené operátory zřetězovat. Jestliže napíšeme
    cin >> i >> j;
    vyhodnotí to překladač jako
    (cin >> i) >> j;
    neboť operátor se vyhodnocuje v pořadí zleva doprava. To znamená, že se přečte hodnota do proměnné i a jako výsledek se vrátí odkaz na proud cin. Takto vrácený odkaz na proud pak slouží jako levý operand při následujícím čtení do proměnné j. Jestliže se při čtení do proměnné i nějakým způsobem změnil stav proudu cin, bude následující operace probíhat již se změněným proudem. Ve třídě istream jsou mimo jiné definovány metody istream::tellg a iostream::seekg. První z nich umožňuje zjistit aktuální pozici v souboru, druhá umožňuje tuto pozici změnit.
    Třída ostream je základem výstupních datových proudů. Je v ní přetížen operátor <<, který slouží k formátovanému výstupu. Definice tohoto operátoru při int má tvar:
    inline ostream& ostream::operator<< (int _i) {
      return *this << (long) _i;
    }
    Tento operátor konvertuje levý operand na hodnotu typu long a použije operátor << pro tento typ; pak vrátí odkaz na proud, pro který jsme jej zavolali. To opět umožňuje zřetězení několika výstupních operátorů v jednom výrazu. V této třídě jsou také definovány metody ostream::tellp a ostream::seekp. První z nich zjistí aktuální pozici v proudu, druhá z nich umožňuje aktuální pozici změnit.
    Třída iostream je společným potomkem tříd istream a ostream. Spojuje jejich vlastnosti, obsahuje tedy prostředky pro vstup i pro výstup. Ke zděděným vlastnostem nepřidává nic nového.
    Třída ostream_withassign je potomkem třídy ostream. Navíc je v ní definován přiřazovací operátor, který umožňuje sdružit objekt této třídy s objektem typu streambuf. Tím, že se změní vyrovnávací paměť, se proud přesměruje. V hlavičkovém souboru iostream.h jsou definovány standardní instance
    extern ostream_withassign cout;
    extern ostream_withassign cerr;
    extern ostream_withassign clog;
    Proud cout slouží k formátovanému výstupu do stdout, proudy cerr a clog představují standardní chybový výstup. Proud cerr není vybaven vyrovnávací pamětí, proud clog je.
    Třída istream_withassign je potomkem třídy istream. Je v ní také definován přiřazovací operátor, který umožňuje sdružit instanci této třídy s objektem typu streambuf a tak jej přesměrovat. V hlavičkovém souboru iostream.h je definována standardní instance
    extern istream_withassign cin;
    která slouží k formátovanému vstupu z stdin.
    Existuje ještě několik dalších tříd datových proudů. Nebudeme se jini již zabývat. Třídy souborových proudů byly již popsány.
    Podívejme se nyní na prostředky pro formátování vstupů a výstupů. Používají se k tomu především manipulátory, což jsou objekty, které lze vkládat do proudů a tím nějak ovlivnit stav proudu (např. změnit formátovací příznaky). Seznam manipulátorů je uveden v následující tabulce.
    Manipulátor Význam
    dec, hex, oct Následující vstupy nebo výstupy v tomto proudu budou probíhat v desítkové, resp. šestnáctkové, resp. osmičkové soustavě.
    setbase(n) Předepisuje číselnou soustavu (při n=8,10 nebo 16) nebo implicitní stav (při n=0).
    endl  Vloží do proudu znak odřádkování a spláchne vyrovnávací paměť.
    ends Vloží na konec řetězce znak '\0'.
    flush Spláchne proud.
    resetiosflags(n)  Vynuluje formátovací příznaky (uložené v x_flags) určené parametrem n.
    setiosflags(n) Nastaví formátovací příznaky (uložené v x_flags) určené parametrem n.
    setfill(n) Definuje vyplňovací znak.
    setprecision(n) Nastaví přesnost reálných čísel.
    setw(n) Nastaví šířku výstupního pole; týká se pouze následující výstupní operace.
    ws Přeskočí na vstupu bílé znaky.
    K nastavování nebo nulování formátovacích příznaků používáme manipulátory setiosflags a resetiosflags. Tyto manipulátory pracují s příznaky, jejichž bity jsou v parametru n rovny 1. Hodnotu prametru pokládáme za bitový součet hodnot jednotlivých formátovacích příznaků. Např.
    cout << setiosflags(ios::showpoint | ios::showpos);
    nastaví příznak zobrazování desetinné tečky (způsobuje také, že se vypisují koncové nuly) a příznak výpisu znaménka.
8. Základy OOP VIII