Kurz C++ (11.)

    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++.

11.1. Kopφrovacφ konstruktor

    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.

11.2. StatickΘ slo₧ky třφdy

    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Θ.

11.3. Co bude přφÜtě?

    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.

Ondřej BuriÜin