Warning
This page is located in archive. Go to the latest version of this course pages. Go the latest version of this page.

5. Přetížené operátory

Třída Vektor

Cílem tohoto cvičení je implementovat třídu s podobnou funkcionalitou, jako má sekvenční kontejner std::vector. Třída bude obsahovat dynamicky alokované pole, které se bude podle potřeb realokovat a poskytne rozhraní v podobě přetížených operátorů pro přístup k jednotlivým prvkům struktury.

Základní verze

Základní verze třídy bude obsahovat dva konstruktory (neparametrický pro vytvoření vektoru nulové délky a konstruktor s jedním parametrem pro rezervaci paměti pro data), privátní metodu resize() pro změnu velikosti vektoru, konstantní veřejnou metedu size() pro zjištění aktuální velikost vektoru, veřejnou metodu push_back() pro vkládání dat na konec vektoru a přetížený operátor []. Konstruktory a metoda size() jsou definovány přímo v deklaraci třídy.

class Vektor
{
    int * pole;
    size_t velikost;
    void resize (size_t x);
public:
    Vektor () : velikost(0) {pole = new int[1];}
    Vektor (size_t x) : velikost(x) {pole = new int[x];}
    size_t size () const {return velikost;}
    void push_back (int x);
    int operator[] (int x);
}

Změna velikosti vektoru

Pro změnu velikosti pole včetně překopírování dat je zde použita strategie vytvoření nového dynamicky alokovaného pole, do kterého je překopírován obsah pomocí metody std::copy, která umožňuje kromě iterátorů pracovat i s běžnými iterátory a aktualizace atributu třídy na nový ukazatel. Všimněte si:

  • nejsou použity C funkce jako realloc nebo memcpy; nepoužívejte je, nemíchejte C a C++ kód
  • před aktualizací atributu pole je třeba dealokovat původně alokovanou paměť; to může způsobit problém v případě, že bychom se pokusili dealokovat pole nulové délky - tato operace není definována, proto je v neparametrickém konstruktoru alokováno pole velikosti 1.

void Vektor::resize (size_t x) 
{
    int* tmp = new int [x];
    std::copy(pole, pole+velikost, tmp); 
    delete [] pole;
    pole = tmp;
}

Vkládání dat na konec

Pro vkládání dat na konec vektoru slouží metoda push_back().

void push_back (int x) 
{
    resize(velikost+1);
    pole[velikost++] = x;
}

Přístup k prvkům vektoru

Pro čtení již zadaných dat vytvoříme přetížený operátor indexace, který vrací položku pole.

int operator[] (int x) 
{
    return pole[x];
}

Hlavní program

V hlavním programu vyzkoušíme vložení 10 čísel typu int do vektoru a vypsání jejich hodnot.

int main()
{
    Vektor a;
 
    for (int i = 0; i < 10; i++)
        a.push_back (i);
 
    for (int i = 0; i < a.size(); i++)
        std::cout << a[i] << " ";
 
    std::cout << std::endl;
 
    return 0;
}

Možná vylepšení

Hotová třída má funkcionalitu, kterou jsme si stanovili zadáním. To ovšem neznamená, že ji nemůžeme trochu vylepšit :-)


Změna prvků vektoru

Po změnu prvků vektoru lze pochopitelně implementovat dedikovanou veřejnou metodu. Velmi jednoduchou úpravou přetíženého operátoru indexace lze ale dosáhnout stejného efektu: pokud totiž bude vracet operátor místo hodnoty referenci, bude možné použít operátor na levé straně výrazu (reference je L-hodnota).

int & operator[] (int x) 
{
    return pole[x];
}

Hlavní funkce main() pak může vypadat třeba takto:

int main()
{
    Vektor b(10);
 
    for (int i = 0; i < b.size(); i++)
        b[i] = i;
 
    for (int i = 0; i < b.size(); i++)
        std::cout << b[i] << " ";
 
    std::cout << std::endl;
 
    return 0;
}

Vlastní iterátor

Určitě by bylo pěkné mít možnost využít pro naši třídu vlastní iterátor a s ním spojený komfort v podobě range for přístupu ke kontejnerům a podobně. Už víme, že iterátor je vlastně variantou ukazatele, musí ovšem respektovat jednotné rozhraní.

Řekněme, že náme následující kód, ve kterém chceme procházet náš kontejner Vektor:

Vektor a(10);
 
for (auto i : a)
{
    std::cout << i;
}

Jak následující kód funguje? Překlač nejprve inicializuje rozsah (range), ve kterém bude kontejner procházet. K tomu potřebuje funkce begin() a end(), které budou vhodným mechanismem vracet ukazatel na první a poslední prvek kontejneru. Takové metody by bylo celkem jednoduché implementovat v rámci třídy Vektor. Ovšem nestačí to: z předchozího cvičení víme, že

  • pro posun iterátoru se používá operace inkrementace ++, návratovou hodnotou je reference na instanci třídy, obsahující inkrementovaný ukazatel;
  • konec iterace se vyhodnotí porovnáním postupně inkrementovaného iterátoru s iterátorem inicializovaným na konec kontejneru operátorem !=, návratovovou hodnotou je instance logického datového typu;
  • pro získání hodnoty odkazované iterátorem je třeba mít k dispozici operátor dereference *, jehož návratovou hodnotou je reference interního datového typu, v tomto případě int.

To znamená přetížení operátorů, ovšem níkoliv ve třídě Vektor, ale v nové třídě, jejíž instance bude inicializována ukazatelem na interní strukturu třídy Vektor a přetížené operátory nové třídy budou zajišťovat potřebnou funkcionalitu.

class Iterator
{
    int * ptr;
public:
    Iterator (int * x) : ptr(x) {}
 
    int& operator*() const { return *ptr; }
    Iterator& operator++() { ptr++; return *this; }
    friend bool operator!= (const Iterator& a, const Iterator& b) { return a.ptr != b.ptr; };
};

V třídě Vektor pak implementujeme potřebné funkce:

class Vektor
{
    //...
public:
    //...
    Iterator begin() {return Iterator(&pole[0]);
    Iterator end()   {return Iterator(&pole[velikost]);
};

Ve hlavním programu pak můžeme vyzkoušet průchod kontejneru pomocí range for:

// kontejner a je uz naplnen cisly od 0 do 9
for (auto i : a)
    std::cout << i << " ";
 
std::cout << std::endl;

Generické funkce

Kontejnery s iterátory mohou být argumenty některých generických funkcí, jako je např. std::accumulate. Následující kód spočítá sumu prvků instance třídy Vektor a vypíše ji na standardní výstup:

std::cout << std::accumulate (a.begin(), a.begin(), 0) << std::endl;

Nepovinným čtvrtým argumentem je ukazatel na funkci, které std::accumulate předává v každé iteraci dvě hodnoty: akumulovanou hodnotu a aktuální prvek získaný iterátorem. Pro následující příklad vytvoříme funkci multiply(int, int), která zpracuje hodnoty předáné volající funkcí a vynásobí je mezi sebou:

int multiply (int a, int b)
{
    return a * b;
}
// ...
std::cout << std::accumulate (a.begin(), a.begin(), 1, multiply) << std::endl;

V generických funkcích se mohou dobře uplatnit lambda funkce. V následujícím příkladu funkce std::accumulate vypočítá aritmetický průměr prvků v kontejneru:

auto lambda = [&](double x, double y) {return x + y / v.size();}
std::cout << std::accumulate (a.begin(), a.end(), 0.0, lambda) << std::endl; 

Jak to funguje?

courses/b2b99ppc/tutorials/05.txt · Last modified: 2021/03/18 10:25 by viteks