Search
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 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.
resize()
size()
push_back()
[]
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); }
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:
C
realloc
memcpy
C++
pole
1
void Vektor::resize (size_t x) { int* tmp = new int [x]; std::copy(pole, pole+velikost, tmp); delete [] pole; pole = tmp; }
Pro vkládání dat na konec vektoru slouží metoda push_back().
void push_back (int x) { resize(velikost+1); pole[velikost++] = x; }
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]; }
V hlavním programu vyzkoušíme vložení 10 čísel typu int do vektoru a vypsání jejich hodnot.
int
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; }
Hotová třída má funkcionalitu, kterou jsme si stanovili zadáním. To ovšem neznamená, že ji nemůžeme trochu vylepšit
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:
main()
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; }
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í.
range for
Řekněme, že náme následující kód, ve kterém chceme procházet náš kontejner Vektor:
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
begin()
end()
++
!=
*
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;
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:
multiply(int, int)
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?