V tomto pokračování kurzu C++ se budeme věnovat následujícím tématům: Nejprve si řekneme, jak rozdělit zdrojový kód do více souborů, což značně zvýší přehlednost. Dále bude následovat přetěžování funkcí, metod a operátorů.
Představme si situaci, kdy máme třídu Buffer v jednom zdrojovém souboru společně s hlavním programem, tedy funkcí main(). Dále můžeme mít třeba třídu BufferManager, která bude zajišťovat správu veškerých instancí třídy Buffer. Prozatím si ukážeme jen deklaraci této třídy a vlastní implementaci této třídy se budeme věnovat až v další kapitole. Třída BufferManager bude obsahovat metody potřebné k obsluze pole instancí třídy Buffer - VytvorBuffer(), SmazBuffer(), VratBuffer() a dalo by se vymyslet mnoho dalších funkcí, které by byly užitečné pro správu paměťových bufferů (např. VytvorBufferZeSouboru()). V následující ukázce je deklarace třídy BufferManager:
class BufferManager {
private :
Buffer* PoleBuffer[MAX_BUFFERS]; // alokace bude dynamicka
int PocetBufferu; // Kolik je prave alokovano
public :
BufferManager();
~BufferManager();
int VytvorBuffer(int _velikost); // Vytvori Buffer a vrati cislo, pres ktere muzeme
// k Bufferu pristupovat pres metodu VratBuffer
// Hodnota 0xFFFFFFFF bude definovana jako chyba
int SmazBuffer(int _poradovecislo); // Maze buffer, v pripade uspechu vrati
poradove cislo
// tohoto bufferu, jinak opet 0xFFFFFFFF
Buffer* VratBuffer(int _poradovecislo);
};
Z rozsáhlosti se dá předpokládat, že pokud bychom tuto třídu ještě přidali do našeho jediného zdrojového souboru, vznikne obrovský, třeba několika tisíc řádkový, soubor. Přidáváním dalších tříd by nám pěkně rostl a za chvíli bychom se v něm přestali orientovat.
Podívejme se na další přístup, takzvané modulární uspořádání programu, což není nic jiného než několik mezi sebou vzájemně spojených zdrojových souborů. Jak bychom tedy vhodně rozdělili výše uvedený příklad do těchto souborů? V případě, že používáte Microsoft Visual C++ je situace velice jednoduchá, u ostatních překladačů ale není o nic složitější. Nejprve vytvoříme projekt pomocí menu daného vývojového prostředí. Potom do tohoto projektu přidáme soubory Buffer.h a Buffer.cpp (opět volby menu). Do souboru Buffer.h vložíme jen deklaraci třídy včetně všech inline metod. Do souboru Buffer.cpp pak vložíme řádek #include "Buffer.h", dále pak veškeré těla metod, popřípadě přetížených operátorů a případné inicializace statických proměnných třídy Buffer. Dvojici souborů Buffer.cpp a Buffer.h nazýváme modulem a měla by to být samostatně přeložitelná část programu. Pro třídu BufferManager bychom udělali to samé, vzniknou tedy soubory BufferManager.h a BufferManager.cpp. Potom přidáme do projektu náš hlavní modul, tedy ten, který bude obsahovat funkci main(), která bude obsahovat instanci třídy BufferManager. Tento modul můžeme například nazvat main.cpp, popřípadě jménem aplikace, kterou vyvíjíme. Nyní je nutné si uvědomit závislosti tohoto programového celku. Je zřejmé, že BufferManager bude pracovat s instancemi třídy Buffer, neboť třída BufferManager bude obsahovat proměnnou typu pole ukazatelů na třídu Buffer. Z toho vyplývá, že BufferManager potřebuje znát rozhraní třídy Buffer, aby znal rozhraní (metody), které může po třídě Buffer požadovat (volat). To zajistíme tím, že použijeme direktivy #include a do souboru BufferManager.cpp na začátek vložíme řádek #include "Buffer.h". Podobně je zřejmé, že hlavní programový modul bude využívat třídu BufferManager k manipulaci s instancemi třídy Buffer, neboť bude volat jeho metody. Tedy do souboru main.cpp vložíme řádek #include "BufferManager.h". Hlavičkové soubory by v nejlepším případě neměly obsahovat další direktivy #include, mohly by totiž vzniknout problémy s vícenásobným vložením jednoho hlavičkového souboru. Toto doporučení se však často porušuje. Problému s vícenásobným vložením souboru se bráníme podmíněným překladem, kde použijeme konstanty:
// Hlavicka.h - doporucena struktura hlavickoveho souboru
#ifndef HLAVICKA_H
#define HLAVICKA_H
// Zde bude vlozeno telo hlavickoveho souboru
#endif
V případě, že konstanta HLAVICKA_H je definována, tělo hlavičkového souboru nebude vloženo, v opačném případě bude. Pro každý hlavičkový soubor je samozřejmě nutné použít jiné konstanty. Výše uvedený příklad ve formě projektu pro Microsoft Visual C++ naleznete v sekci Download (projekt Organizace).
Tento postup má několik výhod. První výhodou je, že pokud nyní změníte něco například v souboru BufferManager.cpp, překladač přeloží jen tento soubor. To má za následek zvýšení rychlosti překladu. V případě jednoho velkého souboru by bylo nutné přeložit ho celý úplně od začátku. Druhou výhodou je, že takto může pracovat na jednom projektu více programátorů. Každý si vezme jeden modul na kterém bude pracovat a ostatním programátorům stačí znát jen rozhraní modulů (tedy hlavičkové soubory) ostatních modulů.
V C++ lze přetěžovat funkce, tedy vlastně definovat více funkcí se stejným jménem. Překladač mezi nimi ovšem potřebuje rozlišit, takže musí mít buď rozdílné typy parametrů nebo různý počet parametrů, popřípadě oboje. K rozlišení překladači nestačí jen, aby tyto funkce měly pouze různé návratové hodnoty. V C++ tedy lze mít funkce:
int Test(int i);
float Test(float f);
int Test(int i, float f);
Protože metody tříd jsou také funkce, lze přetěžování využít i u nich. V ukázce si přetížíme metodu VytvorBuffer(), tak abychom jako parametr mohli použít již existující instanci třídy Buffer. Z požadavku na funkčnost je vidět, že se v těle bude používat kopírovací konstruktor třídy Buffer, který máme již hotový z minulého dílu. V ukázce následuje implementace (tedy vlastně soubor BufferManager.cpp):
// Zdrojovy soubor BufferManager.cpp
#include "stdafx.h" // Pro pouziti predkompilovane
hlavicky v MSVC
#include <stdio.h> // Nejprve systemove hlavickove soubory, potom nase
#include "Buffer.h"
#include "BufferManager.h"
BufferManager::BufferManager()
{
PocetBufferu = 0;
for(int i = 0; i < MAX_BUFFERS; i++)
{
PoleBuffer[i] = NULL;
}
}
BufferManager::~BufferManager()
{
// Musime smazat vsechny naalokovane buffery
for(int i = 0; i < MAX_BUFFERS; i++)
{
if(PoleBuffer[i]) { delete PoleBuffer[i]; }
}
}
int BufferManager::VytvorBuffer(int _velikost)
{
// Nejpve overime, zdali mame jeste vubec misto pomoci promenne PocetBufferu
if(PocetBufferu <= MAX_BUFFERS)
{
// Najdeme misto v poli, kam novy buffer umistime
int i = 0; // Index v poli ktery prave zkoumame
while(i < MAX_BUFFERS)
{
if(!PoleBuffer[i]) { break; }
else { i++; }
}
PoleBuffer[i] = new Buffer(_velikost);
if(PoleBuffer[i])
{
return i; // V pripade uspechu vratime index do pole PoleBuffer
}
}
return 0xFFFFFFFF; // jinak vratime chybovy stav
}
int BufferManager::SmazBuffer(int _poradovecislo)
{
// Overime, jestli buffer, ktery chceme uvolnit je alokovan
if(PoleBuffer[_poradovecislo])
{
delete PoleBuffer[_poradovecislo];
PoleBuffer[_poradovecislo] = NULL; // nastavime priznak, ze je volne misto
return _poradovecislo; // vratime cislo bufferu, ktery jsme zrusili v pripade
uspechu
}
return 0xFFFFFFFF; // jinak opet vratime chybu
}
Buffer* BufferManager::VratBuffer(int _poradovecislo)
{
// jestlize tento buffer existuje, pak vratime ukazatel na tento buffer
if(PoleBuffer[_poradovecislo])
{
return PoleBuffer[_poradovecislo];
}
return NULL; // v pripade neuspechu vratime NULL
}
Konstanta MAX_BUFFERS je definována v hlavičkovém souboru BufferManager.h jako 10. Metody VytvorBuffer() a SmazBuffer() vrací v případě neúspěchu hodnotu 0xFFFFFFFF. Jinak vrací číslo, které identifikuje právě vytvořený buffer. Nyní už tedy dopíšeme jen přetíženou metodu BufferManager::VytvorBuffer(), která bude jako parametr mít ukazatel na třídu Buffer. Do hlavičkového souboru BufferManager.h přidáme řádek:
int VytvorBuffer(Buffer* _zdroj);
Do souboru BufferManager.cpp pak:
int
BufferManager::VytvorBuffer(Buffer *_zdroj)
{
// Nejprve overime, zdali mame jeste vubec misto pomoci
promenne PocetBufferu
if(PocetBufferu <= MAX_BUFFERS)
{ // Najdeme misto v poli, kam novy buffer umistime
int i = 0; // Index v poli ktery
prave zkoumame
while(i < MAX_BUFFERS)
{
if(!PoleBuffer[i]) { break; }
else { i++; }
}
PoleBuffer[i] = new Buffer(*_zdroj);
// Pouzije kopirovaci konstruktor
if(PoleBuffer[i])
{
return i;
// V pripade uspechu vratime index do pole PoleBuffer
}
}
return 0xFFFFFFFF; // jinak vratime chybovy stav
}
Jazyk C++ dále umožňuje přetěžovat operátory, čímž lze rozšířit jejich význam i pro výčtové a objektové datové typy. Pokud si vzpomenete na minulou lekci o kopírovacím konstruktoru, probírali jsme problém s mělkou kopií, kdy se pouze přenesly členské proměnné. S tímto problémem se setkáme i v případě, kdy použijeme operátor = mezi dvěma instancemi stejné třídy. Řešením je právě přetížení operátoru =. Přetížení operátoru se provádí definováním operátorové funkce, jejíž jméno sestává ze slova operator a za ním následuje symbol operátoru, který chceme přetížit. Tedy pro operátor = napíšeme jméno funkce operator =(). Tento přetížený operátor se použije stejným způsobem jako původní operátor. Alternativně ho lze také zavolat pomocí operátorové funkce. Přetěžováním operátorů nelze:
Existují operátory, které přetěžovat nejdou vůbec, jsou to: ., .* , ::, ?:, typeid, const_cast, reinterpret_cast, dynamic_cast, static_cast a sizeof. Operátory preprocesoru # a ## též nelze přetěžovat. Následující operátory je možné přetěžovat jen jako nestatické metody objektových typů: =, (), [], -> a (typ). Poslední je operátor přetypování. Ostatní operátory, vyjma new a delete, lze přetěžovat buď jako nestatické metody objektových typů nebo jako funkce s alespoň jedním parametrem objektového nebo výčtového typu. Operátory new a delete kromě přetížení i předefinovat a lze je přetěžovat jako statické metody objektových typů nebo jako samostatnou funkci bez souvislosti s objektovými nebo výčtovými typy.
Nejprve se budeme zabývat přetěžováním unárních volně přetížitelných operátorů. Jak bylo uvedeno v přehledu, lze je přetěžovat jako nestatické metody nebo jako funkce s alespoň jedním parametrem objektového nebo výčtového typu.
Zde je nutné se zmínit o operátorech ++ a --, které oba existují v prefixové a postfixové verzi. Abychom je oba mohli přetížit, musí být nějak rozlišitelné. To je zajištěno následovně, chceme-li přetížit prefixový operátor deklarujeme operátorovou funkci jako obyčejnou funkci s jedním parametrem nebo jako metodu bez parametrů. Pro přetížení postfixového operátoru definujeme tuto funkci jako obyčejnou funkci se dvěma parametry, z nichž druhý je typu int, nebo jako metodu s jedním parametrem typu int. Pokud navíc po postfixových parametrech chceme aby vracely původní hodnotu, jak je to u standardních verzí těchto operátorů, je nutné si je tak naprogramovat. Přetížení těchto dvou operátorů nad výčtovým typem měsíce si předvedeme v následující ukázce:
enum mesice { leden, unor, brezen, duben,
kveten, cerven, cervenec, srpen, zari, rijen, listopad, prosinec };
mesice operator++(mesice& mes)
{
int i = mes;
i++; // Vyuzijeme operatoru pro cela cisla
if(i > prosinec) { i = leden; }
mes = mesice(i); // Posuneme se na dalsi mesic
return mes; // vratime upravenou hodnotu
}
Pokud bychom přetěžovali postfixový operátor, jeho funkční prototyp by vypadal mesice operator++(mesice &mes, int). Aby fungoval tak, jak postfixový operátor ++ fungovat má, tedy aby vracel původní hodnotu je nutné si v těle na začátku uchovat hodnotu parametru a tu nakonec vrátit. To samé je nutné udělat i pro prefixovou verzi. Příklad s oběma přetíženými operátory naleznete v příslušném souboru v sekci Download (projekt PretezOperatory).
Druhý způsob jak definovat operátorovou funkci je vytvořit nestatickou metodu. To si předvedeme na příkladu implementace třídy pro práci s komplexními čísly:
class complex {
private: double re, im;
public:
complex(double _re = 0, double _im = 0):re(_re), im(_im) { }
double Re() { return re; }
double Im() { return im; }
cplx operator-() { return cplx(-re, -im); }
};
Nyní máme-li dvě instance prvni, druha můžeme napsat přiřazení:
cplx prvni(5.0, 5.0), druha;
druha = -prvni; // V druha bude zaporne komplexni cislo prvni, tedy
(-5.0, -5.0);
druha = prvni.operator-(); // Alternativni zapis
V dalším díle dokončíme přetěžování operátorů. Zmíním se o ukazateli this. Povíme si něco o klíčovém slově friend a konečně načneme téma dědičnosti.
Příště nashledanou.