9. Základy OOP IX
  1. Následující funkce provádí řazení n prvků pole typu int podle velikosti.

  2. void Razeni(int *a, int n){
      for (int w = 0; w < n; ++w){
        int k = w;
        for (int l = w+1; l < n; l++) if (a[l] < a[k]) k = l;
        if (k != w) {
          int s = a[w];
          a[w] = a[k];
          a[k] = s;
        }
      }
    }
    Řazení prvků pole se vyskytuje často a bylo by tedy vhodné tuto funkci vložit do knihovny. Problém je, že ne vždy potřebujeme seřadit prvky typu int. Můžeme vyjít z uvedeného řešení a když budeme potřebovat řadit pole s prvky jiného typu, stačí nahradit všechna klíčová slova int označením nového typu. To ale není nejlepší nápad; co když některé int zapomeneme změnit nebo změníme i int, které se typu prvků řazeného pole netýká. Nabízejí se nám ale dvě rozumnější řešení.
    Všechny výskyty klíčového slova int, které označují typ řazeného pole, nahradíme nějakým obecným identifikátorem, např. TYP a tomuto identifikátoru dáme význam pomocí typedef. Např.
    typedef int TYP;
    Vyzkoušejte změnit naši funkci v tomto smyslu.
  3. Druhé řešení je deklarovat celou funkci jako makro, jehož parametrem bude typ řazeného pole:

  4. #define RAZENI(TYP) void Razeni(TYP *a, int n){              \
      for (int w = 0; w < n; ++w){                               \
        int k = w;                                               \
        for (int l = w+1; l < n; l++) if (a[l] < a[k]) k = l;    \
        if (k != w) {                                            \
          TYP s = a[w];                                          \
          a[w] = a[k];                                           \
          a[k] = s;                                                \
        }                                                        \
      }                                                          \
    }
    Vyzkoušejte v nějakém programu.
  5. Jazyk C++ nabízí lepší řešení předchozího problému a to použití šablony. Naprogramujeme funkci Razeni jen jednou a to jako šablonu. Typ prvků pole bude parametrem této šablony. Překladač pak v případě potřeby vytvoří podle této šablony funkci s požadovanými vlastnostmi (podle typu určeného parametrem šablony). Šablony se v mnohém podobají makrům, zpracovává je ale překladač a ne preprocesor. Deklarace šablony v programu představuje abstraktní vzor, podle kterého je překladač schopen definovat celé skupiny funkcí. Funkce vytvořené podle šablony nazýváme instance šablony. V C++ můžeme deklarovat šablony normálních funkcí, šablony tříd a jejich metod.

  6. Deklaraci šablony můžeme v programu C++ zapsat pouze na úrovni souboru a má tento obecný tvar:
    template<seznam_par>deklarace_šablony
    Za klíčovým slovem template následuje v lomených závorkách seznam formálních parametrů šablony. Jsou to formální parametry oddělené čárkami. Šablony mohou mít typové a hodnotové parametry. Hodnotové parametry jsou parametry, s jakými se setkáváme u obyčejných funkcí. Mohou být skalárních typů. Nelze použít objektové typy nebo pole. Nelze zde také použít výpustku. U hodnotových parametrů můžeme předepsat implicitní hodnoty. Typové parametry jsou uvedeny klíčovým slovem class (u novějších překladačů je zde klíčové slovo typename) a specifikují datové typy. Skutečným parametrem šablony, který odpovídá formálnímu parametru, může být označení libovolného datového typu. Deklarace_šablony znamená deklaraci normální funkce, objektového typu nebo metody podle obvyklých pravidel. V této deklaraci mohou být některé konstanty nahrazeny hodnotovými parametry a některá označení typů typovými parametry.
    Nejdříve se budeme zabývat šablonami řadových funkcí. Šablona řadové funkce musí mít pouze typové parametry. Jako příklad si uvedeme deklaraci šablony funkce Razeni:
    template<class TYP> void Razeni(TYP *a, int n){
      for (int w = 0; w < n; ++w){
        int k = w;
        for (int l = w+1; l < n; l++) if (a[l] < a[k]) k = l;
        if (k != w) {
          TYP s = a[w];
          a[w] = a[k];
          a[k] = s;
        }
      }
    }
    Šablona Razeni má jeden formální parametr TYP, který představuje datový typ. Skutečným parametrem této šablony může být libovolný datový typ, pro který jsou definovány všechny operátory použité pro tento typ v šabloně. Instance šablony normální funkce budou funkce se stejným identifikátorem, které se budou lišit typem parametrů. Můžeme je generovat buď explicitně nebo implicitně. Začneme u implicitního generování instancí. Předpokládejme, že jsme v programu deklarovali výše uvedenou šablonu Razeni. Je-li a pole typu int, pak příkaz
    Razeni(a, 10);
    způsobí, že překladač automaticky vytvoří instanci void Razeni(int *, int). Podobně deklarujeme-li v programu šablonu
    template<class T> T max(T a, T b){
      return a < b ? b : a;
    }
    případně pro novější překladače
    template<typename T> T max(T a, T b){
      return a < b ? b : a;
    }
    a proměnné x a y typu int, způsobí zápis
    int k = max(x, y);
    že se proměnné k přiřadí hodnota většího z čísel uložených v x a y. Bude-li ale z typu char, pak zápis
    z = max(x, z);
    je chybný, neboť takovou funkci podle naší šablony vytvořit nelze. Šablona max totiž předpokládá, že typy obou parametrů jsou shodné. Šablona nenahrazuje prototyp funkce, takže nelze spoléhat na konverzi parametrů. To znamená, že pokud bychom deklarovali v programu konstantu
    const int N = 1000;
    a pokusili se vytvořit instanci šablony Razeni zápisem
    Razeni(a, N);
    protestoval by překladač, že neumí najít funkci Razeni(int *, const int) a my bychom museli použít přetypování
    Razeni(a, (int)N);
    nebo explicitní vytvoření instance jak je uvedeno dále. Pokuste se vyzkoušet šablonu funkce Razeni v nějakém programu.
  7. Explicitní generování funkcí se poněkud liší ve starších a novějších překladačích C++. V nových překladačích (odpovídajících ANSI C++ - např. Borland C++ 5.0) se pro explicitní generování instancí používá syntaxe

  8. template prototyp;
    To znamená, že instanci šablony Razeni vytvoříme zápisem
    template void Razeni(int *, int);
    a instanci šablony max zápisem
    template double max(double, double);
    Tyto deklarace nahrazují prototyp, takže překladač může provádět konverze parametrů.
    Někdy se dostaneme do situace, kdy pro určitý typ parametrů nám instance vytvořená podle šablony nevyhovuje. Nic nám např. nebrání generovat instanci char * max(char *, char *), která bude porovnávat dva znakové řetězce podle adresy. Pro nás má ale podstatně větší význam porovnávání znakových řetězců podle abecedy. V C++ nám naštěstí deklarace šablony nebrání, abychom definovali funkci se stejným jménem a s potřebným typem parametrů:
    char * max(char *a, char *b){
      ...// porovnání řetězců
    }
    Tato deklarace zastíní šablonu (přesněji řečeno pokládá ji za instanci šablony) a nemusíme se obávat, že by překladač podle šablony vytvořil nesmyslnou funkci.
  9. Deklarace šablony třídy nebo metody má opět tvar

  10. template<sez_par>deklarace;
    Středník na konci je podle normy nezbytný. Šablony tříd a jejich metod mohou mít (na rozdíl od šablon normálních funkcí) nejen typové, ale i hodnotové parametry. Skutečné hodnotové parametry při použití šablony musí být konstantní výrazy, tj. musí je umět vyhodnotit již překladač. V této deklaraci můžeme k označení typu použít typové formální parametry a jako konstanty použít hodnotové formální parametry. V deklaraci šablony třídy můžeme zapsat i vložené metody a vložené spřátelené funkce. Ostatní metody musí mít své vlastní šablony. V následujícím příkladu deklarujeme šablonu třídy pro práci s dynamicky alokovaným jednorozměrným polem proměnné délky. Jako parametr šablony zadáme počet prvků pole a jejich typ.
    template<class T, int N =2> class Pole {
      int delka;
      T* p;
    public:
      Pole(T r=0);
      Pole(Pole&);
      ~Pole() {delete p;};
      Pole& operator= (Pole&);
      T& operator[] (int i) {return p[i];}
      Pole operator*(double);
      friend Pole<T,N> operator* (double d, Pole<T,N>& Q) {return Q*d;}
    };
    Tato šablona má typový parametr T a hodnotový parametr N (má implicitní hodnotu). Identifikátor Pole je jméno šablony. V deklaraci šablony třídy je lze používat bez parametrů, při ostatních použitích však musí být spojeno se skutečnými parametry. Pro šablonu třídy, která obsahuje ukazatel na dynamicky alokované pole, jsme museli deklarovat kopírovací konstruktor, destruktor a přiřazovací operátor. Snadný přístup ke složkám tohoto pole nám umožní operátor indexování. Dále jsme zde deklarovali operátor *, který vynásobí všechny prvky pole daným číslem. Operátor násobení číslem zprava deklarujeme jako metodu (první operand je Pole), operátor násobení číslem zleva (v pořadí číslo*Pole) deklarujeme jako spřátelenou funkci. Operátor indexování a destruktor jsme zde deklarovali jako vložené metody, neboť jsme zapsali jejich definiční deklarace v těle šablony třídy. Pro ostatní metody musíme deklarovat zvláštní šablony. Podívejme se na šablonu implicitního a kopírovacího konstruktoru a operátoru násobení číslem zprava:
    template<class T, int N> Pole<T,N>::Pole(T r) : delka(N) {
      p = new T[delka];
      if (!p) Chyba();
      for (int i = 0; i < N; i++) p[i] = r;
    }
    template<class T, int N> Pole<T,N>::Pole(Pole<T,N>& P): delka(P.delka) {
      p = new T[delka];
      if (!p) Chyba();
      for (int i = 0; i < N; i++) p[i] = P.p[i];
    }
    template<class T, int N> Pole<T,N> Pole<T,N>::operator*(double d) {
      Pole<T,N> Q = *this;
      for (int i = 0; i < N; i++) Q.p[i] = Q.p[i]*d;
      return Q;
    }
    Mimo deklaraci šablony třídy musíme spolu se jménem šablony uvádět vždy parametry. Jedinou výjimkou je šablona konstruktoru, kde se parametry uvádějí pouze jednou, ve jménu typu vlevo od dvou dvojteček. Je asi jasné, že šablony metod, které jsou deklarovány samostatně, nejsou součástí deklarace šablony třídy. Méně zřejmé ale může být, že součástí deklarace šablony třídy není ani deklarace vložené spřátelené funkce. Proto jsme v deklaraci druhého operátoru * použili zápis Pole<T,N>.
    Vytvoříme-li instanci šablony třídy, vznikne objektový typ. Pod tím se skrývá nejen jméno a struktura třídy, ale také kódy metod a případné instance statických atributů. Nejjednodušší způsob jak vytvořit instanci třídy je přímé použití v deklaraci. Např. zápis
    Pole<int, 15> Ppp(99);
    způsobí vytvoření typu jména Pole<int, 15>. Současně vznikne instance Ppp nově vytvořeného typu. Obvykle je ale výhodnější vytvořit nejprve nový typ, tedy instanci šablony, pomocí deklarace typedef:
    typedef Pole<int, 15> intpole15;
    Příkazem
    typedef Pole<long> lpole;
    vytvoříme typ Pole<long, 2>, neboť překladač použije implicitní hodnotu parametru N. Norma ANSI C++ umožňuje předepsat explicitní generování třídy zápisem
    template class Pole<int, 2>;
    Klíčové slovo class před identifikátorem je nezbytné. Vyzkoušejte použít šablonu Pole v nějakém programu.
  11. V šabloně třídy můžeme samozřejmě specifikovat také statické složky. Každá instance takovéto šablony, kterou podle této šablony vytvoříme, bude obsahovat své vlastní instance statických složek. V takovém případě ovšem potřebujeme pro každou instanci šablony také definiční deklaraci těchto statických atributů; ta může obsahovat i inicializaci. I zde si ovšem můžeme vypomoci šablonou, tentokrát šablonou statické složky. Např.

  12. template<class T> class vektor {
      static T pocet;
      T p[10];
    public:
      vektor() {for (int i=0; i < 10; i++) p[i] = 0; pocet++;}
      ...
    };
    template<class R> R vektor<R>::pocet = 0;
    // generujeme instance
    vektor<int> t;
    vektor<double> r;
    Instance vektor<int> bude mít statický atribut int vektor<int>::pocet; instance vektor<double> bude mít statický atribut double vektor<double>::pocet. Oba budou inicializovány hodnotou 0. Instance statických atributů se zde vytvoří automaticky při vytváření instancí šablony vektor. Šablonu statické složky můžeme pochopitelně nahradit obyčejnou definiční deklarací. To znamená, že předchozí příklad můžeme zapsat také takto:
    template<class T> class vektor {
      static T pocet;
    ...
    };
    vektor<int> t;
    int vektor<int>::pocet = 0;
    vektor<double> r;
    double vektor
    Pokud nám pro určité hodnoty parametrů nevyhovuje instance vytvořená podle šablony, můžeme ji deklarovat podle svých představ. Má-li překladač chápat nově deklarovaný typ jako instanci existující šablony, musí se jméno typu shodovat se jménem šablony a musí obsahovat skutečné parametry. Např.
    class Pole<float, 100> {
      int p[100];
    public:
      int& operator[] (int i);
      Pole<float, 100>::Pole(int m);
    };
    Pole<float, 100>::Pole(int m = -1) {
      for (int i = 0; i < 100; i++) p[i] = m;
    }
    Pro parametry float a 100 použije překladač instanci, kterou jsme zde předložili; pro ostatní hodnoty vytvoří instance podle šablony.
    Šablony představují mimořádně vhodný nástroj pro vytváření knihoven. Také skladové třídy (třídy představující zásobníky, fronty, seznamy a jiné struktury pro ukládání dat) se vyplatí implementovat jako šablony. Řadu příkladů šablon najdeme v Borlandské knihovně skladových tříd.
  13. Nyní již jsme schopni programovat v jazyku C++. Když ale začneme vytvářet programy pro Windows, pak zjistíme, že to není jednoduché. API Windows (aplikační programové rozhraní) je značně rozsáhlá kolekce C funkcí. Jejich použití si ukážeme na příkladu. Následující část programu zavede a zobrazí bitovou mapu uprostřed obrazovky:

  14. HPALETTE hPal;
    BITMAPFILEHEADER bfh;
    BITMAPINFOHEADER bih;
    LPBITMAPINFO lpbi = 0;
    HFILE hFile;
    DWORD nClrUsed, nSize;
    HDC hDC;
    HBITMAP hBitmap;
    void _huge *bits;
    do {
      if ((hFile = _lopen(data.FileName, OF_READ)) == HFILE_ERROR) break;
      if (_hread(hFile, &bfh, sizeof(bfh)) != sizeof(bfh)) break;
      if (bfh.bfType != 'BM') break;
      if (_hread(hFile, &bih, sizeof(bih)) != sizeof(bih)) break;
      nClrUsed = (bih.biClrUsed) ? bih.biClrUsed : 1 << bih.biBitCount;
      nSize = sizeof(BITMAPINFOHEADER) + nClrUsed * sizeof(RGBQUAD);
      lpbi = (LPBITMAPINFO) GlobalAllocPtr(GHND, nSize);
      if (!lpbi) break;
      hmemcpy(lpbi, &bih, sizeof(bih));
      nSize = nClrUsed * sizeof(RGBQUAD);
      if (_hread(hFile, &lpbi->bmiColors, nSize) != nSize) break;
      if (_llseek(hFile, bfh.bfOffBits, 0) == HFILE_ERROR) break;
      nSize = bfh.bfSize-bfh.bfOffBits;
      if ((bits = GlobalAlocPtr(GHND, nSize)) == NULL) break;
      if (_hread(hFile, bits, nSize) != nSize) break;
      hDC = GetDC(hWnd);
      hBitmap = CreateDIBitmap(hDC, &(lpbi->bmiHeader), CBM_INIT,
                               bits, lpbi, DIB_RGB_COLORS);
      if (hBitmap) {
        LPLOGPALETTE lppal;
        DWORD nsize = sizeof(LOGPALETTE) + (nClrUsed-1) * sizeof(PALETTEENTRY);
        lppal = (LPLOGPALETTE) GlobalAllocPtr(GHND, nSize);
        if (lppal) {
          lppal->palVersion = 0x0300;
          lppal->palNumEntries = (WORD) nClrUsed;
          hmemcpy(lppal->palPalEntry, lpbi->bmiColors, nClrUsed * sizeof(PALETTEENTRY));
          hPal = CreatePalette(lppal);
          (void) GlobalFreePtr(lppal);
        }
      }
    } while(FALSE);
    if (hFile != HFILE_ERROR) _lclose(hFile);
    HPALETTE oldPal = SelectPalette(hDC, hPal, FALSE);
    RealizePalette(hDC);
    HDC hMemDC = CreateCompatibleDC(hDC);
    HBITMAP oldBitmap = (HBITMAP)SelectObject(hMemDC, hBitmap);
    BitBlt(hDC, 0, 0, (WORD)bih.biWidth, (WORD)bih.biHeight, hMemDC, 0, 0, SRCCOPY);
    SelectObject(hMemDC, oldBitmap);
    DeleteDC(hMemDC);
    SelectPalette(hDC, oldPal, FALSE);
    ReleaseDC(hWnd, hDC);
    if (bits) (void)GlobalFreePtr(bits);
    if (lpbi) (void)GlobalFreePtr(lpbi);
    Vidíme, že takový jednoduchý problém jako je zobrazení bitové mapy vyžaduje značně složitý kód. Usnadnění této práce spočívá ve vytváření tříd, které obalují často prováděné programové úlohy Windows, což značně zvýší produktivitu práce programátorů. Po vytvoření zaobalujících tříd je můžeme opakovaně používat. Tím vytváříme pracovní rámce.
    Pracovní rámec je kolekce tříd, které zjednoduššují programování pro Windows zaobalením často používaných technik programování. Pracovní rámce nazýváme knihovny tříd.
    Jedním z pracovních rámců je Object Windows Library (OWL) firmy Borland. S pomocí této knihovny můžeme předchozí část programu zapsat takto:
    TDib dib("Test.bmp");
    TPalette pal(dib);
    TBitmap bitmap(dib, &pal);
    TClientDC dc(*this);
    dc.SelectObject(pal);
    dc.RealizePalette();
    TMemoryDC memdc(dc);
    memdc.SelectObject(bitmap);
    dc.BitBlt(0, 0, bitmap.Width(), bitmap.Height(), memdc, 0, 0);
    Vidíme, že verze OWL je podstatně kratší a také srozumitelnější. Tyto příklady nám ukázaly, co je to pracovní rámec. Pracovní rámec před námi skrývá detaily, které nemusíme znát. Ve skutečnosti tato verze části programu odpovídá první verzi, pouze některé činnosti jsou ukryty v knihovně tříd. Vše co potřebujeme vědět o objektech tvořících pracovní rámec je to, jak je použít v programu.
    Dobré pracovní rámce přebírají plně možnosti OOP. OWL a VCL (Visual Component Library) jsou vzorovými příklady objektově orientovaného programování. Pracovní rámce usnadňují programování. Nevýhodou je to, že program zapsaný pomocí pracovního rámce je delší a pomalejší než stejný program zapsaný přímo v C. Zvýšení produktivity programování tuto nevýhodu ale převáží.
    Jedním z prvních pracovních rámců je OWL firmy Borland. První verze OWL byl samostatný produkt určený pro práci s překladačem Borland C++ verze 3. Postupně vznikaly další verze. Např. od verze OWL 2.5 byla přidána podpora pro OLE v nové množině tříd nazvaných OCF (Object Component Framework). OCF není technicky součástí OWL. Pracuje na stejném principu jako OWL, ale může být použito nezávisle na OWL. Poslední nejnovější verze OWL je 5.
    Někdy mezi OWL 1 a OWL 2 vzniklo MFC (Microsoft Foundation Class Library). MFC je součástí překladače Microsoft Visual C++ a je dostupná i s překladači dalších firem. MFC je knihovnou jiného typu než OWL. MFC nesplňuje zcela požadavky OOP.
9. Základy OOP IX