-
Následující funkce provádí řazení n prvků pole typu int podle
velikosti.
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.
-
Druhé řešení je deklarovat celou funkci jako makro, jehož parametrem bude
typ řazeného pole:
#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.
-
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.
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.
-
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
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.
-
Deklarace šablony třídy nebo metody má opět tvar
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.
-
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ř.
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.
-
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:
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.