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 <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.
}

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 <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.

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ů.

Výchozí kód

 Úkoly k procvičení 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?

Ř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<T>, konstruktor očekává ukazatel na jeden objekt a destruktor používá operátor delete.
  • Pokud je typu 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.

Případ jednotlivého objektu

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.

Případ pole

 Úkoly k procvičení

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>
#include <sstream>
 
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.

int main() {
    std::stringstream sok("3 1 2 3");
    std::stringstream seof("3 1 2");
    std::stringstream sfail("3 1 2 n");
 
    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<double[]>&. 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<double[]>.
  • Odstraňte destruktor třídy vector.

Řešení

courses/b6b36pcc/cviceni/cviceni-05.txt · Last modified: 2022/10/20 11:36 by nagyoing