Kurz C++ (13.)

 V dalším pokračování kurzu o C++ dokončíme povídání o přetěžování operátorů.

13.1. Přetěžování operátorů - pokračování

    Minule jsme probrali unární operátory a dnes se budeme  zabývat operátory binárními. Nejprve si přetížíme volně přetěžovatelný operátor, kterým je například operace sčítání a odčítání. V minulém díle se vyskytla malá chybička, za kterou se omlouvám. V poslední ukázce kódu musíte všechny identifikátory cplx nahradit identifikátorem complex, jinak dostanete varování o nedefinovaném typu cplx.

13.1.1. Binární operátory

    Tyto operátory lze přetěžovat jako obyčejnou funkci se dvěma parametry, alespoň jeden z nich musí být objektového nebo výčtového typu, nebo jako metodu s jedním parametrem. Pro ukázku použijeme opět třídu pro práci s komplexními čísly z minulého dílu:

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; }
    complex operator-(complex b) { return complex(re-b.re, im-b.im); }   
// Odecte dve komplexni cisla
};

Teď si můžeme ukázat použití operátoru:

void main(void)
{
    complex c1, c2, c;
    c = c1 - c2;
    c = c2 - 5;
    c = 5 - c1;
};

    Pokud přeložíte takovýto kód, dostanete varování, že neexistuje globální operátor, který přijímá typ complex za svůj parametr nebo neexistuje příslušná konverze. Je to způsobeno posledním řádkem, protože číslo 5 není objektový typ a nelze pro něj tedy zavolat verzi operátoru přetíženého jako metodu. Pro správnou funkci je tedy třeba vytvořit globální operátor, který bude normální funkcí se dvěma parametry typu complex:

complex operator-(complex a, complex b) { return complex(a.Re()-b.Re(), a.Im()-b.Im()); }

    Po přidání této verze operátoru však dostaneme opět chybu, překladač se nemůže rozhodnout, kterou verzi použít. Je tedy nutné používat vždy jen jednu z verzí. Zavolání operátorové funkce je možné provést i alternativním zápisem:

c = c1.operator-(c2);    // Pro pretizeni jako metoda
c = operator-(5, c1);   
// Pro obecnou funkci

    Zdrojový kód naleznete v sekci Downloads (projekt PretezMinus).

13.1.2. Operátory =, (), [], -> a (typ)

    Tyto operátory lze přetěžovat pouze jako nestatické metody objektových typů. Ukážeme si, jak přetížit operátory přiřazení =, indexování [] a operátor přetypování (typ). Operátor volání funkce () se přetypovává jako metoda s hlavičkou: operator()(parametry).

13.1.2.1. Operátor přiřazení

    O tomto operátoru jsem se již několikrát zmiňoval, především ve spojení s kopírovacím konstruktorem a problémem s mělkou kopií. Připomeneme si, že pokud třída obsahuje dynamickou paměť a požadujeme, aby každá instance měla svoje vlastní, nesdílená data v této paměti, pak je nutné vytvořit kopírovací konstruktor. Nyní se podíváme, co se stane pokud v programu použijeme operátor přiřazení následujícím způsobem:

void main(void)
{
    Buffer prvni(5);
    prvni.NastavNa('a');

    Buffer druhy(10);

    druhy.NastavNa('b');

    druhy = prvni;
}

    Buffer je nám již známá třída z lekce číslo 11. Kód vytvoří dvě instance třídy Buffer, které obsahují dynamicky alokované pole znaků. Po provedení přiřazení však nastane problém, protože pokud nemá třída explicitně definovaný operátor přiřazení, dojde k vytvoření implicitního. Implicitní přiřazovací operátor se chová podobně jako se implicitní kopírovací konstruktor, tedy přenese obsah členských proměnných z instance prvni do instance druhy. To má ale v tomto případě horší následky, nežli použití implicitního kopírovacího konstruktoru. Instance druhy má již alokovanou paměť, na kterou ukazuje členská proměnná třídy Buffer m_Buffer. Přiřazením pak tuto proměnnou přepíšeme, čímž ztratíme ukazatel na tuto paměť, a dojde ke ztrátě paměti. V následující ukázce je tedy přetížený operátor přiřazení:

Buffer& Buffer::operator=(Buffer& Buf)
{
    if(m_Buffer)
    {
        delete(m_Buffer);
        s_PocetByte -= m_Velikost;
        m_Velikost = 0;
    }

    m_Buffer = new char[Buf.m_Velikost];

    if(m_Buffer)
    {
        m_Velikost = Buf.m_Velikost;
        s_PocetByte += m_Velikost;

        for(int i = 0; i < m_Velikost; i++)
        {
            m_Buffer[i] = Buf.m_Buffer[i];
        }
    }

    return *this;
}

    Nejprve uvolníme dynamickou paměť v objektu na levé straně přiřazení, potom vytvoříme pole o velikosti pole objektu, který stojí na pravé straně operátoru přiřazení. Ověříme jestli alokace paměti proběhla správně, pokud ano, pak překopírujeme obsah pole. Jazyk C++ nepředepisuje, co má operátorová funkce přiřazení vracet, tato implementace vrací ukazatel na objekt na levé straně přiřazovacího operátoru, tedy jako standardně definovaný operátor =. O ukazateli this se ještě zmíním.

    Pro optimalizaci by ještě bylo lepší, pokud bychom nejprve zkontrolovali, jestli náhodou nemají oba objekty stejnou velikost dynamicky alokované paměti. Potom by odpadla nutnost uvolňovat a znovu alokovat paměť, která ve srovnání s jedním porovnáním zabere mnohem více času:

Buffer& Buffer::operator=(Buffer& Buf)
{
    if(m_Velikost == Buf.m_Velikost)
    {
        cout << "- just copying\n";
        s_PocetByte -= m_Velikost;
    }
    else
    {
        cout << "- deleting memory\n";
        if(m_Buffer)
        {
            delete(m_Buffer);
            s_PocetByte -= m_Velikost;
            m_Velikost = 0;
        }

        m_Buffer = new char[Buf.m_Velikost];
    }

    if(m_Buffer)
    {
        s_PocetByte += m_Velikost;
        m_Velikost = Buf.m_Velikost;

        for(int i = 0; i < m_Velikost; i++)
        {
            m_Buffer[i] = Buf.m_Buffer[i];
        }
    }

    return *this;
}

    Zdrojový kód naleznete v sekci Downloads (projekt PretezPrir).

13.1.2.2. Operátor indexování

    Tento operátor je nám dobře znám ve spojení s poli. V příkladu tento operátor přetížíme tak, aby pracoval stejným způsobem, bude tedy možné přistupovat k prvkům v dynamicky alokovaném poli právě přes indexy. Operátor přetěžujeme ve tvaru: operator[](parametr). Následuje přetížená verze:

char& Buffer::operator[](int _indx)
{
    if(m_Buffer)
    {
        if(_indx < m_Velikost)
// Osetrime preteceni mezi
        {
            return m_Buffer[_indx];
        }
        else
        {
            cout << "m_Velikost je mensi nez index : " << m_Velikost << endl;
            return m_Buffer[m_Velikost - 1];
        }
    }
    else
    {
       
// jinak vytvorime pole, kam se index vejde
        cout << "vytvarim nove pole" << endl;
        m_Buffer = new char[_indx+1];
// Pole se indexuje od 0
        if(m_Buffer)
        {
            m_Velikost = _indx + 1;
            s_PocetByte += m_Velikost;
            return m_Buffer[_indx];
        }
        else
        {
            cout << "vracim zacatek pole, ale dojde k chybe pristupu do pameti" << endl;
            return *m_Buffer;
        }
    }
}

    Jak vidíte, tak je možné pomocí přetížení provádět kontrolu mezí, ale ještě by bylo potřeba ošetřit poslední případ, kdy stejně musíme vrátit paměť, která není alokovaná. Bylo by možné použít například výjimek, ale v každém případě je lepší, když dojde k chybnému přístupu do paměti pokaždé, než aby aplikace někdy pracovala správně a někdy by přepsala životně důležitá data jiné aplikace nebo dokonce operačního systému. Ještě je vhodné zmínit, že parametr operátoru může být jakéhokoliv typu, takže není problém indexovat pomocí znaků nebo dokonce řetězců.

    Zdrojový kód příkladu je v sekci Downloads (projekt PretezIndx).

13.1.2.3. Operátor přetypování

    Pomocí tohoto operátoru můžeme definovat funkce, které převedou daný objektový typ na jiný objektový nebo i neobjektový typ. Překladač potom může tento operátor využívat i k implicitním konverzím. Pro příklad nám opět poslouží třída Buffer, pro kterou si přetížíme operátor přetypování na (char *):

Buffer::operator char*()
{
    if(m_Buffer) { return m_Buffer; }
    return NULL;
}

    Nyní můžeme používat funkce jako je např. memcpy() pro kopírování bloků paměti, přičemž jako parametr použijeme jméno instance. Překladač totiž použije pro implicitní konverzi námi definovaný operátor.

    Zdrojový kód příkladu je v sekci Downloads (projekt PretezPret).

13.2. Ukazatel this

    V těle každé nestatické metody je k dispozici ukazatel se jménem this, který ukazuje vždy na instanci, pro kterou byla ta daná metoda volána. Tento ukazatel se předává jako skrytý parametr každé metodě. Tuto hodnotu nesmíme v těle funkce měnit a překladač nám to ani neumožní, protože tento ukazatel není l-hodnotou - nemůže stát na levé straně přiřazovacího příkazu. V těle metody complex::Re()im stejný význam jako this->im.

13.3. Klíčové slovo friend

    Toto klíčové slovo umožňuje porušit přístupová práva k prvkům třídy, ve které je uvedeno. V odstavci pro přetížení binárního operátoru jsme vytvořili globální funkci operator-(), v jejímž těle nelze přistupovat k soukromým (private) nebo chráněným (protected) prvkům třídy complex. Pomocí friend:

class complex {
friend complex operator-(complex a, complex b);
private: double re, im;
public:
    complex(double _re = 0, double _im = 0):re(_re), im(_im) { }
    double Re() { return re; }
    double Im() { return im; }
};

complex operator-(complex a, complex b) { return complex(a.re + b.re, a.im + b.im); }

    Zdrojový kód příkladu je v sekci Downloads (projekt Friends1).

    Takto lze zpřístupnit prvky i jiné třídě:  friend class JinaTrida;
    nebo jenom určité metodě jiné třídy:  friend double JinaTrida::Fce1(complex&);.

    V obou případech může členská funkce třídy JinaTrida::Fce1(complex&) přistupovat k prvkům re a im třídy complex.

    Zdrojový kód příkladu je v sekci Downloads (projekt Friends2).

13.4. Operace s celými objekty

   V jazyce C se parametry přenáší hodnotou, kdy překladač vytvoří lokální kopii objektu, se kterou se pak v těle funkce pracuje. Dále nabízí přenos parametrů pomocí ukazatele, kdy se lokální proměnná nevytváří. Druhý způsob se používá pro rozměrná pole a struktury. Vzhledem k tomu, že v C++ může být objektový typ velmi rozsáhlý (co se týče dat) a vytvoření kopie tak může zabrat velmi mnoho času a paměti, není vhodné předávat parametry hodnotou. Lze použít opět buď ukazatele na typ příslušné třídy (a k prvkům objektu přistupovat pomocí operátoru ->) nebo jazyk C++ zavádí možnost použít přenos parametrů odkazem. Pro přenos odkazem stačí parametr definovat jako referenci na typ příslušné třídy (např. Buf& ref_buf) a v těle funkce nebo metody pak přistupovat k objektu pomocí standardní tečkové notace (např. ref_buf.NastavNa('a')). Tento způsob také umožňuje měnit data vně funkce, podobně jako kdybychom předávali parametr pomocí ukazatele.

13.5. Dědičnost a kompozice

    Jak jsme si uvedli tak třída v objektově orientovaných programovacích jazycích slouží především ke spojení dat a operací nad těmito atributy. Jednou z vlastností konstrukce třídy je, že k již stávajícím prvkům můžeme jednoduše přidat další metodu nebo datový prvek. Dědičnost nám právě nabízí tyto možnosti s některými výhodnými vlastnostmi. Třídu, od které dědíme prvky (metody i data) nazýváme třídou základní, bázovou nebo též rodičovskou, třída která vznikne se pak nazývá třídou odvozenou, podtřídou nebo potomkem. Dědičnost se využívá zpravidla tak, že od obecnější třídy odvodíme třídu specializovanější. Jiným možným způsobem vytváření složitějších objektů z jednoduchých je metoda kompozice, kdy jeden objekt obsahuje další objektové typy.

    Rozhodování o tom, jestli novou specializovanější třídu vytvořit metodou dědění nebo kompozice nám může usnadnit takzvaný "je test". Vezměme si jako příklad dvojici CMotor a CMotoroveVozidlo. Můžeme si položit otázku: "Je motorove vozidlo motor?". Odpovědí na tuto otázku samozřejmě je, že není, motorové vozidlo ale určitě obsahuje motor. Naše rozhodnutí tedy bude pro metodu kompozice, kdy ve třídě CMotoroveVozidlo vytvoříme členskou proměnnou m_motor typu CMotor. Na kompozici nás také může přivést "má test", kdy se zeptáme: "Má motorové vozidlo motor?".

    Pro dvojici CZivocich a CPes budeme postupovat úplně stejně, položíme si otázku: "Je pes zivocichem?". Pokud se nepletu, tak odpověď na tuto otázku bude ano a tedy použijeme možnosti vytvořit specializovanější třídu CPes od třídy CZivocich zděděním. Třídě CPes pak přidáme některé speciální metody a data specifická pro psy, mohla by to být třeba proměnná m_hlasitoststekotu a z metod například Stekej().

    Dědičnost je tedy vlastně o hledání společných vlastností skupiny souvisejících objektů. Pokud bychom programovali například aplikaci pro správu zoologické zahrady mohli bychom ještě nalézt společné vlastnosti pro savce, ptáky apod. Od těchto specializovanějších tříd bychom pak odvozovali jednotlivá zvířata. Pokud bychom hledali pečlivě, a s největší pravděpodobností s pomocí zkušeného zoologa, dopracovali bychom se k rozčlenění živočichů do systému živočišné říše, se kterou se každý určitě ve škole setkal.

13.6. Co bude příště

    Příště si ještě povíme něco o přetěžování a předefinování standardních operátorů new a delete a dále se budeme věnovat dědičnosti. Také si ukážeme nějaké diagramy hierarchie tříd. V následujících dílech se pak budeme věnovat obsluze výjimek.

Příště nashledanou.

Ondřej Burišin