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