Kurz C++ (10.)


    Z předchozího článku máme hrubý nástin objektového programování. V dnešním díle se budeme věnovat speciálním metodám - konstruktoru a destruktoru a podíváme se podrobněji na členské funkce. Probereme také vytváření objektů a to jak statické, tak dynamické.
 
10.1. Základy práce s objekty

V uvedeném kódu jsou vynechány direktivy #include. Většina příkladů vyžaduje stdio.h kvůli konstantě NULL a jiným, iostream.h kvůli cout a string.h kvůli manipulaci s řetězci. Pro pokusy s funkcí malloc() je nutné použít hlavičkový soubor malloc.h.

    Nejprve se zaměříme na zmíněný konstruktor. Konstruktor je členská metoda třídy, která je volána jako první při vytváření objektu, přičemž nezáleží na tom, zdali vytváříme objekt staticky nebo dynamicky (viz. níže). Má tedy na starost inicializaci daného objektu (např. alokaci paměti, nastavení určitých proměnných atd.). Aby se metoda stala konstruktorem stačí jediné a to nazvat ji stejným jménem jako třídu a vynechat návratovou hodnotu, protože konstruktory nikdy nic nevrací. Tedy pro třídu Clovek bude metoda s názvem Clovek() konstruktorem. Pokud se podíváte na příklady z minulé lekce, tak si jistě všimnete, že ve třídách konstruktor chybí. To je ale v pořádku, jestliže totiž konstruktor explicitně nedeklarujete, dosadí si překladač takzvaný implicitní konstruktor, což je vlastně prázdná funkce. V případě, že je to vhodné lze samozřejmě napsat více konstruktorů, které budou mít rozdílné parametry nebo různý počet parametrů, překladač si pak vybere ten správný právě podle parametrů, které mu předáte.

    Teď, když víte základní informace o konstruktoru, je asi jasné k čemu bude dobrý destruktor. Je to tedy opět metoda třídy, která je zodpovědná za zrušení objektu (tedy např. uvolnění paměti alokované v konstruktoru). Destruktor se vyznačuje opět stejným jménem jako třída, ale navíc má předsazen znak "~". Opět nevrací žádnou hodnotu a navíc musí být bezparametrický. Každá třída může mít pouze jeden destruktor.

    Nyní si uvedeme krátkou ukázku konstruktoru a destruktoru. Pro následující jednoduché a krátké třídy použiji zápis metod přímo v definici třídy. Pro rozsáhlejší třídy je však vhodnější zapsat delší členské funkce vně tuto definici, jak bylo uvedeno v minulé lekci. Ale zpět k naší ukázkové třídě  :

class Buffer {

private :  
            BYTE *m_Buffer;   
// BYTE je definovan jako typedef unsigned char BYTE;

public :   
            Buffer(int _velikost) { m_Buffer = new BYTE[_velikost]; }
            ~Buffer() { delete[] Buffer; }

            void ZapisNa(int _pozice, BYTE _hodnota) { m_Buffer[_pozice] = _hodnota; }
};

    Tento kód vypadá na první pohled celkem rozumně, ale co by se stalo, kdyby náhodou nebylo možné přidělit paměť o velikosti _velikost? Z konstruktoru nelze vrátit návratovou hodnotu indikující chybu. Proto bychom měli nejprve nastavit proměnnou m_Buffer (členské proměnné je lepší pro přehlednost značit předponou m_, která nám zkracuje member variable. Pro zápis parametrů pak používám _ a za ním jméno parametru. V těle procedury pak jasně vidím, s kterou proměnnou pracuji) na předem dohodnutou hodnotu, v tomto případě tedy nejspíše NULL a v destruktoru a členských metodách pak kontrolovat, zdali je m_Buffer platný (tedy ne-NULLový). Jinak bychom voláním funkce ZapisNa() způsobili chybný přístup do paměti. Přepsáno do kódu tedy :

class Buffer {

private :
            BYTE *m_Buffer;

public :   
            Buffer(int _velikost)    { m_Buffer = new BYTE[_velikost]; }
            ~Buffer() { if(m_Buffer) { delete[] m_Buffer; m_Buffer = NULL;  } }

            void ZapisNa(int _pozice, BYTE _hodnota) { if (m_Buffer) { m_Buffer[_pozice] = _hodnota; } }
};

    Jinou možností by pak bylo využívat konstruktor jen k nastavení proměnných a vytvořit členskou funkci Init(int _velikost), která by nám vracela např. TRUE v případě úspěchu a FALSE v případě neúspěchu. Dále bychom vytvořili jednu soukromou (private) proměnnou m_JeInit typu boolean. Tuto proměnnou bychom v případě úspěšného provedení funkce Init() nastavili na TRUE, jinak na FALSE. Pak bychom ji testovali v každé metodě, jestliže bude TRUE, pak víme, že pole BYTEů má přidělenou paměť a přístup do něj tedy nezpůsobí chybný přístup do paměti. Opět si ukážeme kód :

class Buffer {

private :
            BYTE *m_Buffer;
            BOOL m_JeInit;

public :   
            Buffer() { m_Buffer = NULL; m_JeInit = FALSE; }
            ~Buffer() { if(m_Buffer) { if(m_JeInit) { delete[] m_Buffer; m_Buffer = NULL; } }

            BOOL Init(int _velikost) { m_Buffer = new BYTE[_velikost]; if(m_Buffer) { m_JeInit = TRUE; } return m_JeInit; }
            void ZapisNa(int _pozice, BYTE _hodnota) { if (m_JeInit) { m_Buffer[_pozice] = _hodnota; } }
};

    Mně osobně se zamlouvá více první způsob, ale záleží jen na vás, který si vyberete. V dalším kódu budu používat způsob první. V výše uvedeném příkladu se skrývá ještě jedna vada na kráse a to, že neověřujeme meze v členské funkci ZapisNa(), je tedy možné zapsat mimo alokované pole, což určitě není to nejlepší. Tento problém lze ale jednoduše vyřešit a to přidáním další členské soukromé proměnné m_Velikost, kterou nastavíme po úspěšném přidělení paměti a pak ve funkci ZápisNa() ověříme, jestli jsme náhodou nepřekročili mez.

    Ještě se podíváme na takzvanou inicializační část, kterou je možno použít u konstruktorů, zavedeme si novou třídu Clovek, která bude mít jako datové prvky proměnné m_Vek a m_Vyska:

class Clovek {

private :
         BYTE m_Vek; int m_Vyska;   
// m_Vek jsem dal jako BYTE z duvodu usetreni pameti,
// protoze cloveka starsiho nez 255 let asi tezko potkate ;))

public :
         Clovek(BYTE _Vek, int _Vyska) : m_Vek(_Vek), m_Vyska(_Vyska) { ; }
};

    Vidíme, že konstruktor Clovek() nemá žádné tělo. Členské proměnné nastavíme pomocí inicializační části, která se píše hned za uzavírající závorku argumentů, oddělíme ji ":" a pak napíšeme proměnnou, do které chceme přiřadit a za ni do závorky hodnotu, kterou jí chceme přiřadit. Výsledný efekt bude stejný, jako kdybychom do těla konstruktoru napsali následující příkazy:

m_Vek = _Vek; m_Vyska = _Vyska;

    Je pěkné, že sice víte k čemu je konstruktor a destruktor, ale abychom mohli třídu použít a vytvořit její instanci, musíme vědět jak toho v programu dosáhnout. Například celočíselnou hodnotu (int) můžeme alokovat buď staticky nebo dynamicky. Nejprve se podívejme na statickou, kterou jsme používali od začátku, napíšeme jméno proměnné (int, real a jméno proměnné). Překladač tak přesně ví, kolik paměti po něm chceme a tak ji pro nás alokuje. Dynamickou alokaci musíme použít tam, kde předem nevíme, jak velká data budou. Třídu Buffer můžeme chtít použít třeba jen pro 5 bytů, ale také třeba pro několik desítek megabyte. Tato paměť se alokuje za běhu programu pomocí funkce malloc() (v klasickém C) nebo pomocí operátoru new (v C++).  To samé lze provést s třídami. Nejprve ukáži nějaký kód, např. funkci main() ve které budeme chtít vytvořit instance třídy Buffer:

void main(void)
{

    Buffer mujStatickyBuffer(10);    // Tedy statická alokace
    Buffer *mujDynamickyBuffer = new Buffer(15); // Dynamická alokace

    // Ted provedeme nejake operace s bufferem
    mujStatickyBuffer.ZapisNa(5, 5);
    mujDynamickyBuffer->ZapisNa(10,8);

    // Pred skoncenim musime dynamicke promenne zrusit
    delete(mujDynamickyBuffer);
}

    Uvedený kód nám tedy vytvoří jeden statický a jeden dynamický objekt. Je mezi nimi několik rozdílů a to v přístupu k členským funkcím, u statického používáme tečkové notace, kdežto u dynamického musíme použít symbolu ->, protože je to vlastně ukazatel. Další rozdíl je v tom, že staticky vytvořené proměnné se po výstupu z bloku nebo funkce samy zruší. Naopak proměnné, které si vytvoříme dynamicky musíme sami explicitně uvolnit, jinak dojde ke ztrátě paměti (tzv. MEMORY LEAK). Liší se také umístění těchto paměťových bloků v paměti. Pokud vytvoříme globální proměnnou, pak je uložena v tzv. datovém segmentu programu, kde se nachází po celou dobu chodu programu. Lokální proměnné, vytvořené v těle nějaké funkce nebo bloku (uzavřeného mezi složené závorky) jsou pak uloženy na zásobníku (STACK) a jsou zrušeny při výstupu z této funkce nebo bloku. A konečně pokud si zažádáme o paměť pomocí malloc() nebo new je vymezena paměť na hromadě (HEAP), doba její existence závisí jen na tom, kdy se rozhodneme vrátit ji systému. Uveďme si tedy ještě kratičký příklad na obecné alokace:

void main(void)
{
    // Nejdrive se na to podivame v C

    int i;    // Tedy statická alokace, proměnná bude na stacku
    int *p_i = NULL; // Pointer na int a nastavíme na NULL, abychom mohli otestovat jestli byla alokace úspěšná

    p_i = (int *)malloc(1*sizeof(int)); // Alokujeme 1 velikost integeru. Musíme explicitně přetypovat, jinak si překladač bude stěžovat

    if(p_i) { // Vždy raději ověřujte, jestli je proměnná platná

       *p_i = 5; // dáme na místo kam ukazuje p_i číslo 5

       // Nějaké operace s integerem na adrese p_i
       // ....
       // ....

       // Uvolnění paměti funkce free()
      
free(p_i);
    }

    // Ted to same v C++

    // To same lze zapsat pomoci new
  
 p_i = NULL;
   
p_i = new int; // Zde nemusíte ukazatel přetypovávat

    if(p_i) {

       *p_i = 6;

       // Nějaké operace s integerem na adrese p_i
       // ....
       // ....

       // Uvolnění paměti operátoru delete
   
   delete p_i;
    }
}

    Nyní opustíme téma konstruktorů a destruktorů a podíváme se na členské metody, o nichž toho zatím mnoho nevíme. Metod je více druhů. Nejprve se zmíním o tzv. metodách nastav/vrať (set/get). Již víme, že v objektovém programování využíváme výhod zapouzdření, tedy neviditelnosti datových členů z vnějšího prostředí. Proto pro většinu datových členů naprogramujeme jednoduché členské funkce, které nastavují/vrací jejich hodnotu. Tyto funkce jsou většinou tak kratičké, že je lze umístit přímo do deklarace třídy. Vytvoříme si novou ukázkovou třídu Clovek:

class Clovek {

private :   BYTE m_Vek;
            int m_Vyska;

public :    Clovek(BYTE _Vek, int _Vyska) : m_Vek(_Vek), m_Vyska(_Vyska) { ; }
            void nastavVysku(int _NovaVyska) { m_Vyska = _NovaVyska; }
            int vratVysku() { return m_Vyska; }
            void nastavVek(BYTE _NovyVek) { m_Vek = _NovyVek; }
            BYTE vratVek() { return m_Vek; }
};

    Pro kratičké funkce je vhodné uvést modifikátor inline, který překladači říká, že bychom byli rádi, kdyby místo volání této funkce nahradil její výskyt přímo tělem této funkce. Překladač nás může, ale nemusí poslechnout. Důvodem této maličkosti je jisté uspoření času, které zabere volání funkce, musí se totiž uložit vzhledem k velikosti této funkce hodně informací (kam se vrátit, stav registrů, atp.). Inline funkce je vlastně makro, které nám ale navíc nabízí typovou kontrolu. Další modifikátor, který můžeme použít je modifikátor const, který uvedeme za ukončující závorku argumentů. Tento modifikátor říká překladači, že daná funkce nemodifikuje objekt, pro který byla volána. Tedy const je vhodné uvést u všech metod, které jen vrací hodnoty, ale pro metody které nastavují hodnoty jej nelze použít. Tedy např. funkce vratVysku() by mohla vypadat takto :

inline vratVysku() const { return m_Vyska; }

    Další metody se kterými se setkáme jsou operace nad objektem, může to být například procedura, která vypíše pro nás významné datové členy. Dnešní lekci zakončíme trošku ucelenějším programem. Ukážeme si také zápis metody mimo deklaraci třídy a rozšíříme našeho člověka o jméno:

#include <stdio.h>
#include <iostream.h>
#include <string.h>

typedef unsigned char BYTE;

class Clovek {

private :
             BYTE m_Vek;
             int m_Vyska;
             char *m_Jmeno;

public :
            Clovek(char *Jmeno, BYTE _Vek, int _Vyska);
            ~Clovek() { if(m_Jmeno) { delete[] m_Jmeno; } }

            void Starni(BYTE _OKolik) { m_Vek += _OKolik; }
            void nastavVysku(int _NovaVyska) { m_Vyska = _NovaVyska; }
            int vratVysku() const { return m_Vyska; }
            void nastavVek(BYTE _NovyVek) { m_Vek = _NovyVek; }
            BYTE vratVek() const { return m_Vek; }
            void Vypis();
};

Clovek::Clovek(char *Jmeno, BYTE _Vek, int _Vyska)  : m_Vek(_Vek), m_Vyska(_Vyska)
{
    m_Jmeno = NULL;

    if(Jmeno) {
        int delka = strlen(Jmeno) + 1;   
// Kvuli poslednimu '\0'

        m_Jmeno = new char[delka];

        if(m_Jmeno) {
            strncpy(m_Jmeno, Jmeno, delka);
        }
    }
}

void Clovek::Vypis()
{
    if(m_Jmeno) { cout << "Jmeno : " << m_Jmeno << '\n'; }
    else { cout << "Bezejmenny" << '\n'; }

    cout << "Vek : " << (int)m_Vek << '\n';
    cout << "Vyska : " << m_Vyska << '\n';
}

int main(int argc, char* argv[])
{
    Clovek muj("Pepik Frantik",10,158);

    muj.Vypis();
    muj.Starni(5);
    muj.Vypis();

    char c;
    cin >> c; 
   // Cekame na stisk klavesy

    return 0;
}

    Budete-li mít chuť, můžete si doprogramovat členské funkce nastavJmeno(), vratJmeno() a třeba ještě funkci Povyrost(). V příštím díle si povíme o kopírovacím konstruktoru, přetěžování členských funkcí, o statických členských proměnných a metodách, jak vytvářet přehledné moduly (jak dělit třídy do souborů) a pravděpodobně začneme s dědičností.

Příště nashledanou.

Ondřej Burišin