Kurz C++ (12.)

 V tomto pokračování kurzu C++ se budeme věnovat následujícím tématům: Nejprve si řekneme, jak rozdělit zdrojový kód do více souborů, což značně zvýší  přehlednost.  Dále bude následovat přetěžování funkcí, metod a operátorů.

12.1.    Organizace projektu

    Představme si situaci, kdy máme třídu Buffer v jednom zdrojovém souboru společně s hlavním programem, tedy funkcí main(). Dále můžeme mít třeba třídu BufferManager, která bude zajišťovat správu veškerých instancí třídy Buffer. Prozatím si ukážeme jen deklaraci této třídy a vlastní implementaci této třídy se budeme věnovat až v další kapitole. Třída BufferManager bude obsahovat metody potřebné k obsluze pole instancí třídy Buffer - VytvorBuffer(), SmazBuffer(), VratBuffer() a dalo by se vymyslet mnoho dalších funkcí, které by byly užitečné pro správu paměťových bufferů (např. VytvorBufferZeSouboru()). V následující ukázce je deklarace třídy BufferManager:

class BufferManager {
private :
    Buffer* PoleBuffer[MAX_BUFFERS];
// alokace bude dynamicka
    int PocetBufferu;
// Kolik je prave alokovano
public :
    BufferManager();
    ~BufferManager();

    int VytvorBuffer(int _velikost); // Vytvori Buffer a vrati cislo, pres ktere muzeme
                                     // k Bufferu pristupovat pres metodu VratBuffer
                                     // Hodnota 0xFFFFFFFF bude definovana jako chyba
    int SmazBuffer(int _poradovecislo); // Maze buffer, v pripade uspechu vrati poradove cislo
                                        // tohoto bufferu, jinak opet 0xFFFFFFFF
    Buffer* VratBuffer(int _poradovecislo);
};

    Z rozsáhlosti se dá předpokládat, že pokud bychom tuto třídu ještě přidali do našeho jediného zdrojového souboru, vznikne obrovský, třeba několika tisíc řádkový, soubor. Přidáváním dalších tříd by nám pěkně rostl a za chvíli bychom se v něm přestali orientovat.

    Podívejme se na další přístup, takzvané modulární uspořádání programu, což není nic jiného než několik mezi sebou vzájemně spojených zdrojových souborů. Jak bychom tedy vhodně rozdělili výše uvedený příklad do těchto souborů? V případě, že používáte Microsoft Visual C++ je situace velice jednoduchá, u ostatních překladačů ale není o nic složitější. Nejprve vytvoříme projekt pomocí menu daného vývojového prostředí. Potom do tohoto projektu přidáme soubory Buffer.h a Buffer.cpp (opět volby menu). Do souboru Buffer.h vložíme jen deklaraci třídy včetně všech inline metod. Do souboru Buffer.cpp pak vložíme řádek #include "Buffer.h", dále pak veškeré těla metod, popřípadě přetížených operátorů a případné inicializace statických proměnných třídy Buffer. Dvojici souborů Buffer.cpp a Buffer.h nazýváme modulem a měla by to být samostatně přeložitelná část programu. Pro třídu BufferManager bychom udělali to samé, vzniknou tedy soubory BufferManager.h a BufferManager.cpp. Potom přidáme do projektu náš hlavní modul, tedy ten, který bude obsahovat funkci main(), která bude obsahovat instanci třídy BufferManager. Tento modul můžeme například nazvat main.cpp, popřípadě jménem aplikace, kterou vyvíjíme. Nyní je nutné si uvědomit závislosti tohoto programového celku. Je zřejmé, že BufferManager bude pracovat s instancemi třídy Buffer, neboť třída BufferManager bude obsahovat proměnnou typu pole ukazatelů na třídu Buffer. Z toho vyplývá, že BufferManager potřebuje znát rozhraní třídy Buffer, aby znal rozhraní (metody), které může po třídě Buffer požadovat (volat). To zajistíme tím, že použijeme direktivy #include a do souboru BufferManager.cpp na začátek vložíme řádek #include "Buffer.h". Podobně je zřejmé, že hlavní programový modul bude využívat třídu BufferManager k manipulaci s instancemi třídy Buffer, neboť bude volat jeho metody. Tedy do souboru main.cpp vložíme řádek #include "BufferManager.h". Hlavičkové soubory by v nejlepším případě neměly obsahovat další direktivy #include, mohly by totiž vzniknout problémy s vícenásobným vložením jednoho hlavičkového souboru. Toto doporučení se však často porušuje. Problému s vícenásobným vložením souboru se bráníme podmíněným překladem, kde použijeme konstanty:

// Hlavicka.h - doporucena struktura hlavickoveho souboru
#ifndef HLAVICKA_H
  #define HLAVICKA_H
  // Zde bude vlozeno telo hlavickoveho souboru
#endif

V případě, že konstanta HLAVICKA_H je definována, tělo hlavičkového souboru nebude vloženo, v opačném případě bude. Pro každý hlavičkový soubor je samozřejmě nutné použít jiné konstanty. Výše uvedený příklad ve formě projektu pro Microsoft Visual C++ naleznete v sekci Download (projekt Organizace).

    Tento postup má několik výhod. První výhodou je, že pokud nyní změníte něco například v souboru BufferManager.cpp, překladač přeloží jen tento soubor. To má za následek zvýšení rychlosti překladu.  V případě jednoho velkého souboru by bylo nutné přeložit ho celý úplně od začátku. Druhou výhodou je, že takto může pracovat na jednom projektu více programátorů. Každý si vezme jeden modul na kterém bude pracovat a ostatním programátorům stačí znát jen rozhraní modulů (tedy hlavičkové soubory) ostatních modulů.

12.2. Přetěžování funkcí a metod

    V C++ lze přetěžovat funkce, tedy vlastně definovat více funkcí se stejným jménem. Překladač mezi nimi ovšem potřebuje rozlišit, takže musí mít buď rozdílné typy parametrů nebo různý počet parametrů, popřípadě oboje. K rozlišení překladači nestačí jen, aby tyto funkce měly pouze různé návratové hodnoty. V C++ tedy lze mít funkce:

    int Test(int i);
    float Test(float f);
    int Test(int i, float f);

    Protože metody tříd jsou také funkce, lze přetěžování využít i u nich. V ukázce si přetížíme metodu VytvorBuffer(), tak abychom jako parametr mohli použít již existující instanci třídy Buffer. Z požadavku na funkčnost je vidět, že se v těle bude používat kopírovací konstruktor třídy Buffer, který máme již hotový z minulého dílu. V ukázce následuje implementace (tedy vlastně soubor BufferManager.cpp):

// Zdrojovy soubor BufferManager.cpp

#include "stdafx.h"    // Pro pouziti predkompilovane hlavicky v MSVC

#include <stdio.h> // Nejprve systemove hlavickove soubory, potom nase

#include "Buffer.h"
#include "BufferManager.h"

BufferManager::BufferManager()
{
    PocetBufferu = 0;
    for(int i = 0; i < MAX_BUFFERS; i++)
    {
        PoleBuffer[i] = NULL;
    }
}

BufferManager::~BufferManager()
{
    // Musime smazat vsechny naalokovane buffery
    for(int i = 0; i < MAX_BUFFERS; i++)
    {
        if(PoleBuffer[i]) { delete PoleBuffer[i]; }
    }
}

int BufferManager::VytvorBuffer(int _velikost)
{
    // Nejpve overime, zdali mame jeste vubec misto pomoci promenne PocetBufferu
    if(PocetBufferu <= MAX_BUFFERS)
    {
        // Najdeme misto v poli, kam novy buffer umistime
        int i = 0; // Index v poli ktery prave zkoumame
        while(i < MAX_BUFFERS)
        {
            if(!PoleBuffer[i]) { break; }
            else { i++; }
        }

        PoleBuffer[i] = new Buffer(_velikost);
        if(PoleBuffer[i])
        {
            return i; // V pripade uspechu vratime index do pole PoleBuffer
        }
    }

    return 0xFFFFFFFF; // jinak vratime chybovy stav
}

int BufferManager::SmazBuffer(int _poradovecislo)
{
    // Overime, jestli buffer, ktery chceme uvolnit je alokovan
    if(PoleBuffer[_poradovecislo])
    {
        delete PoleBuffer[_poradovecislo];
        PoleBuffer[_poradovecislo] = NULL; // nastavime priznak, ze je volne misto

        return _poradovecislo; // vratime cislo bufferu, ktery jsme zrusili v pripade uspechu
    }

    return 0xFFFFFFFF; // jinak opet vratime chybu
}

Buffer* BufferManager::VratBuffer(int _poradovecislo)
{
    // jestlize tento buffer existuje, pak vratime ukazatel na tento buffer
    if(PoleBuffer[_poradovecislo])
    {
        return PoleBuffer[_poradovecislo];
    }

    return NULL; // v pripade neuspechu vratime NULL
}

Konstanta MAX_BUFFERS je definována v hlavičkovém souboru BufferManager.h jako 10. Metody VytvorBuffer() a SmazBuffer() vrací v případě neúspěchu hodnotu 0xFFFFFFFF. Jinak vrací číslo, které identifikuje právě vytvořený buffer. Nyní už tedy dopíšeme jen přetíženou metodu BufferManager::VytvorBuffer(), která bude jako parametr mít ukazatel na třídu Buffer. Do hlavičkového souboru BufferManager.h přidáme řádek:

int VytvorBuffer(Buffer* _zdroj);

Do souboru BufferManager.cpp pak:

int BufferManager::VytvorBuffer(Buffer *_zdroj)
{
    // Nejprve overime, zdali mame jeste vubec misto pomoci promenne PocetBufferu
    if(PocetBufferu <= MAX_BUFFERS)
    { // Najdeme misto v poli, kam novy buffer umistime
        int i = 0; // Index v poli ktery prave zkoumame
        while(i < MAX_BUFFERS)
        {
            if(!PoleBuffer[i]) { break; }
            else { i++; }
        }

        PoleBuffer[i] = new Buffer(*_zdroj); // Pouzije kopirovaci konstruktor
        if(PoleBuffer[i])
        {
            return i; // V pripade uspechu vratime index do pole PoleBuffer
        }
    }

    return 0xFFFFFFFF; // jinak vratime chybovy stav
}

12.3. Přetěžování operátorů

   Jazyk C++ dále umožňuje přetěžovat operátory, čímž lze rozšířit jejich význam i pro výčtové a objektové datové typy. Pokud si vzpomenete na minulou lekci o kopírovacím konstruktoru, probírali jsme problém s mělkou kopií, kdy se pouze přenesly členské proměnné. S tímto problémem se setkáme i v případě, kdy použijeme operátor = mezi dvěma instancemi stejné třídy. Řešením je právě přetížení operátoru =. Přetížení operátoru se provádí definováním operátorové funkce, jejíž jméno sestává ze slova operator a za ním následuje symbol operátoru, který chceme přetížit. Tedy pro operátor = napíšeme jméno funkce operator =(). Tento přetížený operátor se použije stejným způsobem jako původní operátor. Alternativně ho lze také zavolat pomocí operátorové funkce. Přetěžováním operátorů nelze:

    Existují operátory, které přetěžovat nejdou vůbec, jsou to: ., .* , ::, ?:, typeid, const_cast, reinterpret_cast, dynamic_cast, static_cast a sizeof. Operátory preprocesoru # a ## též nelze přetěžovat. Následující operátory je možné přetěžovat jen jako nestatické metody objektových typů: =, (), [], -> a (typ). Poslední je operátor přetypování. Ostatní operátory, vyjma new a delete, lze přetěžovat buď jako nestatické metody objektových typů nebo jako funkce s alespoň jedním parametrem objektového nebo výčtového typu. Operátory new a delete kromě přetížení i předefinovat a lze je přetěžovat jako statické metody objektových typů nebo jako samostatnou funkci bez souvislosti s objektovými nebo výčtovými typy.

    Nejprve se budeme zabývat přetěžováním unárních volně přetížitelných operátorů. Jak bylo uvedeno v přehledu, lze je přetěžovat jako nestatické metody nebo jako funkce s alespoň jedním parametrem objektového nebo výčtového typu.

12.3.1. Unární operátory

    Zde je nutné se zmínit o operátorech ++ a --, které oba existují v prefixové a postfixové verzi. Abychom je oba mohli přetížit, musí být nějak rozlišitelné. To je zajištěno následovně, chceme-li přetížit prefixový operátor deklarujeme operátorovou funkci jako obyčejnou funkci s jedním parametrem nebo jako metodu bez parametrů. Pro přetížení postfixového operátoru definujeme tuto funkci jako obyčejnou funkci se dvěma parametry, z nichž druhý je typu int, nebo jako metodu s jedním parametrem typu int. Pokud navíc po postfixových parametrech chceme aby vracely původní hodnotu, jak je to u standardních verzí těchto operátorů, je nutné si je tak naprogramovat. Přetížení těchto dvou operátorů nad výčtovým typem měsíce si předvedeme v následující ukázce:

enum mesice { leden, unor, brezen, duben, kveten, cerven, cervenec, srpen, zari, rijen, listopad, prosinec };
mesice operator++(mesice& mes)
{   
   int i = mes;
   i++;    // Vyuzijeme operatoru pro cela cisla
   if(i > prosinec) { i = leden; }
   mes = mesice(i);    // Posuneme se na dalsi mesic
   return mes;    // vratime upravenou hodnotu
}

    Pokud bychom přetěžovali postfixový operátor, jeho funkční prototyp by vypadal mesice operator++(mesice &mes, int). Aby fungoval tak, jak postfixový operátor ++ fungovat má, tedy aby vracel původní hodnotu je nutné si v těle na začátku uchovat hodnotu parametru a tu nakonec vrátit. To samé je nutné udělat i pro prefixovou verzi. Příklad s oběma přetíženými operátory naleznete v příslušném souboru v sekci Download (projekt PretezOperatory).

    Druhý způsob jak definovat operátorovou funkci je vytvořit nestatickou metodu. To si předvedeme na příkladu implementace třídy pro práci s komplexními čísly:

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; }
    cplx operator-() { return cplx(-re, -im); }
};

    Nyní máme-li dvě instance prvni, druha můžeme napsat přiřazení:

cplx prvni(5.0, 5.0), druha;
druha = -prvni;    // V druha bude zaporne komplexni cislo prvni, tedy (-5.0, -5.0);
druha = prvni.operator-();    // Alternativni zapis

12.4. Co bude příště?

    V dalším díle dokončíme přetěžování operátorů. Zmíním se o ukazateli this. Povíme si něco o klíčovém slově friend a konečně načneme téma dědičnosti.

Příště nashledanou.

Ondřej Burišin