Vítejte u další části kurzu o objektovém programování. Dnes se budeme věnovat kopírovacímu konstruktoru, statickým složkám třídy. Slibované "pravé" objektové programování si tedy necháme pravděpodobně až na přespříští díl. K této části je také přiložen soubor, který obsahuje všechny ukázky kódu probírané v této lekci. Soubory jsou organizovány jako projekty pro Microsoft Visual C++.
Nejprve se podívejme, co se stane, jestliže okopírujeme objekt pomocí operátoru "=". Mějme tedy následující kód :
class CTrida {
private :
int m_Cislo;
char m_Znak;
public :
CTrida(int _Cislo = 1, char _Znak = 'a') : m_Cislo(_Cislo), m_Znak(_Znak) { ; }
void Vypis() {
cout << "Tento objekt obsahuje : " << m_Cislo << " a " << m_Znak << endl; }
};
void main(void)
{
CTrida prvni(5,'c');
prvni.Vypis();
CTrida druha = prvni;
prvni.Vypis();
druha.Vypis();
char c; // Cekame na stisk klavesy
cin >> c;
}
Příkazem CTrida druha=prvni se provede něco podobného jako kdybychom přiřazovali jednoduché proměnné, tedy na místo v paměti pro druha se přenesou data z paměti pro prvni. Výsledkem tedy budou dvě stejné instance objektu. Z kódu je vidět, že za tímto účelem jsme nenapsali ani řádku navíc. Jak se tedy toto "kouzlo" provedlo? Jednoduše, překladač vytvořil tzv. implicitní kopírovací konstruktor, jehož jediným účelem je přenést složku po složce z instance prvni do instance druha. Nyní si ukážeme malý problém: Co se stane, jestliže třída obsahuje nějakou dynamicky alokovanou paměť? V implicitním konstruktoru se nic nezmění, tedy přenesou se hodnoty všech složek z jedné instance do druhé. To ovšem nemusí být přesně to, co chceme udělat, protože pak máme dvě instance, které sdílejí tuto dynamicky alokovanou paměť. Jestliže tedy pomocí instance druha změníte data v této paměti, změna se promítne i do objektu prvni. Nejhorší případ by mohl nastat, kdybychom jednu z instancí úplně zrušili a použili bychom druhou instanci k přístupu k této dynamické paměti. Takováto akce by vedla k chybnému přístupu do paměti a Windows by naši aplikaci okamžitě ukončily. Právě proto máme k dispozici speciální metodu - kopírovací konstruktor, jehož pomocí můžeme ovlivnit proces kopírování jednotlivých složek Mějme tedy třídu, která ve svém těle obsahuje dynamicky alokovanou paměť, řekněme třeba pole a ukažme si, co je potřeba udělat, aby kopírování fungovalo správně. Tedy aby výsledkem byly dvě na sobě nezávislé instance se stejnými daty. Využijeme přitom třídu Buffer z minulého dílu, kterou trochu upravíme, abychom si mohli zkontrolovat výsledek našeho snažení:
class Buffer {
private :
char *m_Buffer;
int m_Velikost;
public :
Buffer(int _velikost)
{
m_Buffer = NULL;
m_Velikost = 0;
m_Buffer = new char[_velikost];
if(m_Buffer) { m_Velikost = _velikost; } // V pripade uspechu nastavime velikost
}
~Buffer() { if(m_Buffer) { delete[] m_Buffer; m_Buffer = NULL; } }
void NastavNa(char hodnota)
{
if(m_Buffer) {
for(int i = 0; i < m_Velikost; i++)
{ m_Buffer[i] = hodnota; }
}
}
void ZapisNa(int _pozice, char _hodnota)
{
if(m_Buffer && (_pozice < m_Velikost)) // Jestlize jsme v mezich
{ m_Buffer[_pozice] = _hodnota; }
}
void Vypis()
{
if(m_Buffer)
{
for(int i = 0; i < m_Velikost; i++)
{ cout << m_Buffer[i] << "|"; }
cout << endl; // Radek mezi dvema vypisy
}
}
};
Jak vidíte, je to jen rozšířená třída Buffer z minulého dílu o pár metod. Metoda NastavNa() nastaví celé pole na hodnotu předanou jako parametr. Za povšimnutí stojí snad jen ověřování přístupu do dynamického pole, aby omylem nedošlo k zápisu do paměti, která nám nepatří. Předvedeme si nyní problémový kód:
void main(void)
{
Buffer prvni(5);
prvni.NastavNa('a');
prvni.Vypis();
Buffer druhy = prvni;
prvni.Vypis();
druhy.Vypis();
druhy.ZapisNa(4, 'k');
prvni.Vypis();
druhy.Vypis();
char c;
cin >> c;
}
Po spuštění tohoto kódu dojde k naplnění objektu prvni hodnotou 'a', potom si uděláme kontrolní Vypis(). Nyní přichází na řadu přiřazení, jehož následkem se objekt překopíruje složku po složce. Po výpisu je zřejmé, že pole mají stejný obsah. Příkaz druhy.ZapisNa(4, 'k') nám demonstruje závislost obsahu obou instancí, neboť po výpisu vidíme, že obě pole jsou opět stejná. Pokud si program spustíte, budete svědky dalšího problému, a to při ukončení. Vysvětlení je jednoduché: Proměnné jsme alokovali staticky, takže budou zrušeny při výstupu z funkce main(). Po zrušení prvního objektu (teď první berme jako ten, který se opravdu bude rušit první) bude blok paměti, na který ukazuje členská proměnná m_Buffer, neplatný a při druhém uvolnění dojde k chybě, která má za následek ukončení naší aplikace systémem Windows. Je pravda, že v tomto případě to není tak důležité (kromě toho, že takhle se programovat nemá), ale kdybychom měli tyto instance alokované dynamicky a jednu jsme zrušili například v průběhu nějakého výpočtu, pak to je větší problém (tuto situaci najdete v projektu kopkonstr2 v přiloženém souboru). Nyní se tedy pokusíme vytvořit náš vlastní kopírovací konstruktor. Hlavička kopírovacího konstruktoru vypadá následovně: Jmeno_Tridy(Jmeno_Tridy& promenna). Konkrétně pro třídu Buffer to tedy bude: Buffer(Buffer& p). Typ Buffer& se nazývá referencí na třídu Buffer. Tady je první pokus, ukážeme si jen tuto metodu, protože zbytek třídy se nezmění:
Buffer(Buffer& puvodni)
{
cout << "KopKonst called" << endl; // Abychom videli, ze byl volan
m_Buffer = new char[puvodni.m_Velikost];
// Pokud se buffer vytvori, pak nastavime velikost pole
if(m_Buffer) { m_Velikost = puvodni.m_Velikost; }
// Prekopirujeme vsechny prvky
for(int i = 0; i < puvodni.m_Velikost; i++)
{
m_Buffer[i] = puvodni.m_Buffer[i];
}
}
Do funkce jsme si přidali pomocný ladicí text, abychom poznali, kdy je tento konstruktor volán. Jako parametr pak při použití dostaneme instanci, která stojí na pravé straně rovnítka. Ještě si shrneme všechny možnosti, jak zavolat kopírovací konstruktor. Nejprve dvě ekvivalentní možnosti pro staticky vytvořenou třídu:
A nyní opět možnost pro dynamicky vytvořenou třídu :
Pokud jste zklamaní, že nelze vytvořit dvě instance a pak jen použít rovnítko (např. treti = prvni), pak nebuďte, protože zanedlouho se dostaneme k přetěžování metod a posléze operátorů, kde se tím určitě budeme zabývat.
Třída může, kromě klasických (nestatických) proměnných, obsahovat také zvláštní proměnné, takzvané statické. Tyto proměnné nejsou závislé na existenci instance dané třídy a jsou sdíleny mezi všemi instancemi. Takové proměnné můžeme například použít pro počítání právě existujících instancí v paměti. V konstruktoru vždy hodnotu o jedna zvýšíme a v destruktoru opět snížíme. V případě třídy Buffer bychom například mohli udržovat celkový počet bytů, které máme pomocí instancí těchto tříd naalokovány. Statické proměnné se deklarují stejně jako klasické, ale uvedeme před nimi klíčové slovo static. Jako příklad si ukážeme právě počítání obsazené paměti instancemi třídy Buffer.
class Buffer {
private :
char *m_Buffer;
int m_Velikost;
static int s_PocetByte;
public :
Buffer(int _velikost)
{
m_Buffer = NULL;
m_Velikost = 0;
m_Buffer = new char[_velikost];
if(m_Buffer) { // V pripade uspechu nastavime velikost a pricteme k nasi staticke promenne
m_Velikost = _velikost;
s_PocetByte += m_Velikost; }
}
Buffer(Buffer& puvodni)
{
m_Buffer = new char[puvodni.m_Velikost];
// Pokud se buffer vytvori, pak nastavime velikost pole
if(m_Buffer) {
m_Velikost = puvodni.m_Velikost;
s_PocetByte += m_Velikost;
}
// Prekopirujeme vsechny prvky
for(int i = 0; i < puvodni.m_Velikost; i++)
{
m_Buffer[i] = puvodni.m_Buffer[i];
}
}
~Buffer() {
if(m_Buffer) {
delete[] m_Buffer; m_Buffer = NULL;
s_PocetByte -= m_Velikost;
}
};
void NastavNa(char hodnota)
{
if(m_Buffer) {
for(int i = 0; i < m_Velikost; i++)
{ m_Buffer[i] = hodnota; }
}
}
void ZapisNa(int _pozice, char _hodnota)
{
if(m_Buffer && (_pozice < m_Velikost)) // Jestlize jsme v mezich
{ m_Buffer[_pozice] = _hodnota; }
}
void Vypis()
{
if(m_Buffer)
{
for(int i = 0; i < m_Velikost; i++)
{ cout << m_Buffer[i] << "|"; }
cout << endl; // Radek mezi dvema vypisy
}
}
static int Stav() { return s_PocetByte; }
};
int Buffer::s_PocetByte = 0; // Pocatecni inicializace
Pokud je tedy úspěšně vytvořena instance třídy Buffer, přičte se počet bytů, které tento nový objekt spravuje, k celkovému počtu bytů. Na počátku je hodnota nastavena na 0, jak vidíme z posledního řádku. Povšimněte si, že i když je tato proměnná private, lze ji tímto způsobem inicializovat. Zjistit hodnotu této statické proměnné můžeme buď přímým přístupem k této proměnné, pokud bychom ji ovšem dali do sekce public této třídy. Pro přístup lze pak použít následující možnosti:
Protože se při objektovém programování snažíme o zapouzdřenost dat, použijeme pro přístup k této statické proměnné členskou metodu. Tato metoda je klasickou vrať metodou, tedy v těle má jen příkaz return s_PocetByte. Před touto metodou by mělo být uvedeno klíčové slovo static. Hlavička bude tedy mít tvar static int Stav(). Tím překladači řekneme, že v těle této procedury přistupujeme pouze ke statickým datům, která existují i v případě, že neexistují žádné instance této třídy. Jestliže nebude klíčové slovo static uvedeno, nebude možné zavolat tuto metodu pomocí zápisu Buffer::Stav(), protože nestatické metody nejsou pomocí tohoto volání dostupné. Je to logické, protože kdybychom v těle této metody přistupovali k nějaké nestatické proměnné, překladač by nemohl určit, kam ji přiřadit, protože nemusí existovat ani jedna instance nebo jich naopak mohou existovat tisíce. Pro zavolání funkce Stav() můžeme použít stejné příkazy jako v případě proměnné.
V dnešním díle se nám třídy poněkud více rozrostly, takže příště bude slibované dělení tříd do souborů, které v dalších dílech věnovaných dědičnosti velmi oceníme. Probrat bychom také měli přetěžování funkcí a operátorů. Nashledanou u příštího dílu.