{{page>courses:b6b36pjc:styles#common&noheader&nofooter}}
{{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}}
===== Cvičení 5: Životní cyklus objektů =====
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.
==== Vznik ====
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
int main() {
std::vector vec1; // vec1 začne existovat při provedení tohoto řádku a je použít tzv. základní konstruktor
std::vector vec2(2); // vec2 začne existovat při provedení tohoto řádku a je použit konstruktor který bere 1 číselný argument.
}
==== Zánik ====
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
int main() {
std::vector vec1; // vec1 začne existovat při provedení tohoto řádku a je použít tzv. základní konstruktor
std::vector 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.
===== Od struktury ke třídě =====
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.
==== Metody třídy ====
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ů.
{{:courses:b6b36pjc:cviceni:cviceni_5_struktura.zip|Výchozí kód}}
Příklad zkompilujte a pokračujte podle následujících instrukcí:
* Změňte strukturu ''vector'' na třídu a změňte viditelnost jejích datových položek na ''public''.
* Vytvořte destruktor třídy ''vector''. Přesuňte do něj kód z metody ''dispose'' a metodu ''dispose'' smažte.
* Změňte funkce ''reserve'', ''push_back'', ''pop_back'', ''size'', ''capacity'', ''clear'', ''at'', a ''resize'' na metody třídy ''vector''.
* Vytvořte základní (default) konstruktor třídy ''vector'', který nastaví ''m_data'' na ''nullptr'', ''m_capacity'' na 0 a ''m_size'' na 0.
* Vytvořte konstruktor třídy ''vector'' se dvěma parametry:
* 1. parametr typu ''size_t'' určuje velikost a kapacitu nového vektoru,
* 2. parametr typu ''double'' určuje hodnotu, na kterou se mají nastavit všechny položky ve vektoru.
* Změňte viditelnost položek ''m_data'', ''m_capacity'' a ''m_size'' na ''private''.
* Zkuste zaměnit klíčové slovo ''class'' za ''struct''. Co se stane?
{{:courses:b6b36pjc:cviceni:cviceni_5_trida.zip|Řešení}}
===== Správa prostředků pomocí unique_ptr =====
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:
* Konstruktor očekává ukazatel. Říkáme, že tento ukazatel ''unique_ptr'' vlastní.
* Destruktor smaže vlastněný ukazatel.
* Pokud je typu ''unique_ptr'', konstruktor očekává ukazatel na jeden objekt a destruktor používá operátor ''delete''.
* Pokud je typu ''unique_ptr'', 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.
==== Případ jednotlivého objektu ====
Mějme následující příklad:
#include
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 // pro unique_ptr
...
int main() {
std::unique_ptr ptr(new my_class);
std::unique_ptr 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();
auto ptr2 = std::make_unique();
ptr->stuff();
ptr2->stuff();
}
Jako parametry funkce ''make_unique'' jsou použity jako parametry konstruktoru
objektu ''my_class''.
==== Případ pole ====
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
#include
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(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
#include
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);
}
==== Vektor s unique_ptr ====
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.
* Vytvořte přetížení funkce ''resize_array'', které umí změnit velikost pole, které funkci poskytneme v parametru s typem ''std::unique_ptr&''. Budou se hodit následující metody třídy ''unique_ptr'':
* Metoda ''.get()'' poskytne ukazatel, který je obsažen uvnitř ''unique_ptr''.
* Metoda ''.swap()'' vzájemně prohodí pole dvou ''unique_ptr''.
* Změňte typ položky ''m_data'' na ''std::unique_ptr''.
* Odstraňte destruktor třídy ''vector''.
{{:courses:b6b36pjc:cviceni:cviceni_5_unique_ptr.zip|Řešení}}