Na minulém cvičení jsme pracovali s dynamicky alokovanými poli a vytvořili si část vlastního vektoru. Dnes si projdeme třídy, jejich životní cyklus a uděláme z našeho vektoru třídu.
Objekt může vzniknout dvěma způsoby. První je deklarací proměnné jeho typu, kdy objekt vznikne ve chvíli provádění daného řádku. Druhý způsob vzniku objektu je volání operátoru new
nebo new[]
, kdy objekt vznikne po provedení alokace paměti.
Při vzniku objektu se vždy zavolá konstruktor. Konstruktorů může být více; který se zavolá záleží na způsobu vytvoření objektu, například prostá deklarace zavolá tzv. základní kostruktor.
#include <vector> int main() { std::vector<int> vec1; // vec1 začne existovat při provedení tohoto řádku a je použít tzv. základní konstruktor std::vector<int> vec2(2); // vec2 začne existovat při provedení tohoto řádku a je použit konstruktor který bere 1 číselný argument. }
K zániku objektu může dojít také dvěma způsoby. První je, že proměnná daného typu tzv. opustí scope, neboli přestane být platná. Druhý je, že je na ukazateli volán operátor delete
nebo delete[]
, kdy nejdříve zanikne objekt a pak je systému vrácena dynamicky alokovaná paměť, kterou objekt obýval.
Při zániku objektu se vždy volá destruktor. Třída může mít destruktor pouze jeden.
#include <vector> int main() { std::vector<int> vec1; // vec1 začne existovat při provedení tohoto řádku a je použít tzv. základní konstruktor std::vector<int> vec2(2); // vec2 začne existovat při provedení tohoto řádku a je použit konstruktor který bere 1 číselný argument. } // zde zanikne vec2 i vec1 a je zavolán jejich destruktor.
Třídy jsou v C++ velmi podobné strukturám. Pokud vezmeme definici vektoru z minulého cvičení
struct vector { double* data = nullptr; size_t capacity = 0; size_t size = 0; };stačí zaměnit klíčové slovo
struct
za klíčové slovo class
. Pokud to uděláme, zjistíme, že naše staré funkce přestaly fungovat. To proto, že součásti třídy jsou soukromé (private). Pokud chceme mít přístup k částem třídy, třeba k metodám, i mimo třídu, pak musíme použít klíčové slovo public:
.
class vector { public: double* data = nullptr; size_t capacity = 0; size_t size = 0; };
Nyní by všechno mělo fungovat jako předtím.
Vyzkoušíme si psaní metod pro třídu vector
. Stáhněte si příklad se strukturou vector
z minulého cvičení, rozdělený do několika souborů.
Příklad zkompilujte a pokračujte podle následujících instrukcí:
vector
na třídu a změňte viditelnost jejích datových položek na public
.
vector
. Přesuňte do něj kód z metody dispose
a metodu dispose
smažte.
vector
, který nastaví m_data
na nullptr
, m_capacity
na 0 a m_size
na 0.
reserve
, push_back
, pop_back
, size
, capacity
, clear
, at
, a resize
na metody třídy vector
.
vector
se dvěma parametry:
size_t
určuje velikost a kapacitu nového vektoru,
double
určuje hodnotu, na kterou se mají nastavit všechny položky ve vektoru.
m_data
, m_capacity
a m_size
na private
.
class
za struct
. Co se stane?
Pokud jsme dynamicky alokovali paměť, je potřeba ji i dealokovat. V minulém cvičení jsme používali new
nebo new[]
k alokaci paměti a delete
nebo delete[]
k dealokaci. V tomto cvičení jsme vytvořili třídu vector
, která se za nás stará o pole čísel typu double
. Předlohou pro náš vector
je std::vector
ze standardní knihovny, který se umí starat o pole různých typů.
Ve standardní knihovně také najdeme třídu unique_ptr
, která se stará o ukazatel na alokovanou paměť. Přitom rozlišuje mezi tím, zda-li vlastní ukazatel na jednotlivý objekt, nebo na pole. Důležité vlastnosti třídy unique_ptr
jsou:
unique_ptr
vlastní.
unique_ptr<T>
, konstruktor očekává ukazatel na jeden objekt a destruktor používá operátor delete
.
unique_ptr<T[]>
, konstruktor očekává ukazatel na pole a destruktor používá operátor delete[]
.
Pokud tedy vložíme ukazatel do konstruktoru objektu unique_ptr
, automaticky se nám postará o správné uvolnění paměti.
Mějme následující příklad:
#include <iostream> class my_class { public: my_class() { std::cout << "Hello\n"; } ~my_class() { std::cout << "Bye Bye\n"; } void stuff() { std::cout << "Stuff\n"; } }; int main() { auto* ptr = new my_class; auto* ptr2 = new my_class; ptr->stuff(); ptr2->stuff(); }
Když tento kód spustíme, ani jeden z objektů typu my_class
se nesmaže. Tomu zabráníme pomocí unique_ptr
tak, že ukazatele vložíme do konstruktoru unique_ptr
, nejlépe ihned z operátoru new
.
#include <memory> // pro unique_ptr ... int main() { std::unique_ptr<my_class> ptr(new my_class); std::unique_ptr<my_class> ptr2(new my_class); ptr->stuff(); ptr2->stuff(); }
Všimněte si, že není potřeba měnit způsob volání metody stuff
. Je to proto, že unique_ptr
je tzv. chytrý ukazatel (smart pointer), a díky tomu operace ptr->něco
a *ptr
mají stejný efekt, jako předtím.
Díky unique_ptr
se teď oba objekty správně smažou. Pomocí funkce std::make_unique
lze navíc dosáhnout poněkud kompaktnějšího zápisu:
int main() { auto ptr = std::make_unique<my_class>(); auto ptr2 = std::make_unique<my_class>(); ptr->stuff(); ptr2->stuff(); }
Jako parametry funkce make_unique
jsou použity jako parametry konstruktoru objektu my_class
.
Následující kód má v sobě několik chyb. Pokuste se je najít a opravit, nejdřív bez použití komplexních typů a poté za použití std::unique_ptr
.
#include <iostream> #include <algorithm> void nacti_a_secti_cisla(std::istream& in, std::ostream& out) { int how_many; in >> how_many; if (!in.good()) { return; } int* numbers = new int[how_many]; int sum = 0; for (int n = 0; n < how_many; ++n) { in >> numbers[n]; sum += numbers[n]; } if (in.fail()) { return; } double average = sum / static_cast<double>(how_many); std::sort(numbers, numbers + how_many); int median = numbers[how_many / 2]; out << "suma zadanych cisel: " << sum << '\n'; out << "prumer zadanych cisel: " << average << '\n'; out << "median zadanych cisel: " << median << '\n'; }
Zde je funkce main
s pár testy.
#include <iostream> #include <sstream> int main() { std::stringstream sok("3 1 2 3"); std::stringstream seof("3 1 2"); std::stringstream sfail("3 1 2 n"); sfail.exceptions(std::ios_base::failbit); nacti_a_secti_cisla(sok, std::cout); nacti_a_secti_cisla(seof, std::cout); nacti_a_secti_cisla(sfail, std::cout); }
Třídu unique_ptr
můžeme také zakomponovat do našeho návrhu třídy vector
. Výhodou bude, že odpadne nutnost vytvářet vlastní destruktor. Protože varianta unique_ptr
pro pole poskytuje operátor indexace pole []
, bude potřeba překvapivě málo úprav v našem stávajícím kódu.
resize_array
, které umí změnit velikost pole, které funkci poskytneme v parametru s typem std::unique_ptr<double[]>&
. Budou se hodit následující metody třídy unique_ptr
:
.get()
poskytne ukazatel, který je obsažen uvnitř unique_ptr
.
.swap()
vzájemně prohodí pole dvou unique_ptr
.
m_data
na std::unique_ptr<double[]>
.
vector
.