Chci geek triko! Chci geek triko!
Extra 10 % bodů navíc a tričko zdarma při zadání kódu "TRIKO10"

Lekce 4 - Destruktory a aplikace konstruktorů v C++

C a C++ C++ Objektově orientované programování Destruktory a aplikace konstruktorů v C++

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Hrací kostka v C++ a konstruktory, jsme si popsali syntaxi konstruktorů a to včetně pokročilých konstrukcí jako byl delegující konstruktor a volání konstruktorů pro atributy. Dnes si popíšeme destruktory a ukážeme si hlavní účel, pro který jsou konstruktory a destruktory použity.

Destruktory

Podobně jako konstruktor, který se volá ihned po vytvoření instance, se destruktor volá automaticky před smazáním instance. Mazání obecně probíhá na konci bloku (tedy konec funkce nebo při uzavírající složené závorce). Destruktor se zapisuje jako metoda, která začíná vlnovkou (~), po které následuje název třídy. Destruktor nikdy nemá parametry a nevrací hodnotu. Takový základní destruktor nám již Visual Studio vygenerovalo a je prázdný (pokud destruktor nedodáme, kompilátor automaticky vytvoří destruktor s prázdným tělem):

Kostka.h

class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();  // deklarace destruktoru
    int pocet_sten;
};

Kostka.cpp

Kostka::~Kostka()  // prázdný destruktor
{
}

Abychom viděli, kdy se destruktor volá, přidáme si do jeho implementace výpis do konzole:

#include <iostream>  // pokud v souboru chybí
using namespace std;  // pokud v souboru chybí
Kostka::~Kostka()
{
    cout << "Volani destruktoru pro kostku s " << pocet_sten << " stenami" << endl;
}

Do main.cpp si vložíme následující kód, který ukazuje případy, kdy se destruktor volá.

void funkce(Kostka k)
{
    cout << "Funkce" << endl;
}
int main()
{
    Kostka prvni(1);
    if (true)
    {
        Kostka druha(2);
        funkce(druha);
        cout << "Funkce skoncena" << endl;
    }
    // cin.get();
    return 0;
}
#include <iostream>
#ifndef __KOSTKA_H__
#define __KOSTKA_H__
using namespace std;
class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
};
#endif
#include <iostream>
#include "Kostka.h"
using namespace std;
Kostka::Kostka() : Kostka(6)
{
    cout << "Volani bezparametrickeho konstruktoru" << endl;
}

Kostka::Kostka(int _pocet_sten)
{
    cout << "Volani konstruktoru s parametrem" << endl;
    pocet_sten = _pocet_sten;
}

Kostka::~Kostka()
{
    cout << "Volani destruktoru pro kostku s " << pocet_sten << " stenami" << endl;
}

Výpisy vidíme na výstupu aplikace:

Konzolová aplikace
Volani konstruktoru s parametrem
Volani konstruktoru s parametrem
Funkce
Volani destruktoru pro kostku s 2 stenami
Funkce skoncena
Volani destruktoru pro kostku s 2 stenami
Volani destruktoru pro kostku s 1 stenami

Když si příklad rozebereme, zjistíme, že destruktor se volá před ukončovacími složenými závorkami a na konci funkce. Tehdy již není proměnná potřeba a C++ provede její odstranění z paměti. Pro lepší pochopení přikládám kód s komentáři.

void funkce(Kostka k)
{
    cout << "Funkce" << endl;
} // destruktor pro "k"

int main()
{
    Kostka prvni(1); // první konstruktor
    if (true)
    {
        Kostka druha(2); // druhý konstruktor
        funkce(druha);
        cout << "Funkce skoncena" << endl;
    } // destruktor pro "druha"
    // cin.get(); pokud volání necháme, neuvidíme mazání "prvni"
    return 0;
} // destruktor pro "prvni"

Konstruktory jsou vypsané také, protože jsme ponechali kód z minulé lekce. Mohlo by vás zarazit, že jsou volány tři destruktory, ale pouze dva konstruktory. V jednom případě se volá kopírující konstruktor, ale tím se budeme zabývat v jiné lekci. Prozatím nám stačí vědět, kdy je destruktor volán.

Konstruktor pro inicializaci

Nyní se podíváme na jeden případ, kdy se nám konstruktory hodí - inicializace třídy.

Definujme na kostce metodu hod(), která nám vrátí náhodné číslo od 1 do počtu stěn. Je to velmi jednoduché, metoda nebude mít žádný parametr a návratová hodnota bude typu int. Náhodné číslo získáme tak, že zavoláme funkci rand() z knihovny cstdlib.

Kostka.h

#ifndef __KOSTKA_H__
#define __KOSTKA_H__

class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
};
#endif

Kostka.cpp

#include <iostream>
#include <cstdlib>
#include "Kostka.h"
using namespace std;
// ... již definované metody
int Kostka::hod()
{
    return rand() % pocet_sten + 1;
}

rand() vrací pseudo náhodné číslo. Aby bylo v požadovaném rozsahu, musíme na něj použít %pocet_sten. Jednička se přičítá proto, aby náhodná čísla byla od jedničky a ne od nuly. Pseudonáhodné číslo znamená, že se začne na nějakém čísle a nějakou operací se od něj dopočítávají zbývající čísla. To má jednu nevýhodu - do main.cpp napište následující kód (ten původní můžete smazat):

#include <iostream>
#include "Kostka.h"
#include "Arena.h"

using namespace std;


int main()
{
    Kostka kostka;
    for (int i = 0; i < 10; i++)
        cout << kostka.hod() << " ";
    cin.get();
    return 0;
}

Všimněte si, že pokud program spustíme opakovaně, vždy generuje stejné čísla (i když by je měl generovat náhodně). To je z důvodu, že výchozí číslo pro generování je pokaždé stejné. My potřebujeme, aby se při každém spuštění začínalo od jiného čísla, toho docílíme pomocí metody srand(), do které předáme aktuální čas. A protože to vlastně nastavuje instanci, tento kód vložíme do konstruktoru.

Ponz.: Vedle knihovny cstdlib musíme includovat i knihovnu ctime.

//main.cpp
int main()
{
    Kostka kostka;
    for (int i = 0; i < 10; i++)
        cout << kostka.hod() << " ";
    cin.get();
    return 0;
}
#ifndef __KOSTKA_H__
#define __KOSTKA_H__

class Kostka
{
public:
    Kostka();
    Kostka(int _pocet_sten);
    ~Kostka();
    int pocet_sten;
    int hod();
};
#endif
#include "Kostka.h"
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

Kostka::Kostka() : Kostka(6)
{
    cout << "Volani bezparametrickeho konstruktoru" << endl;
}

Kostka::Kostka(int _pocet_sten)
{
    cout << "Volani konstruktoru s parametrem" << endl;
    pocet_sten = _pocet_sten;
    srand(time(NULL));
}
// zbývající metody

Kostka::~Kostka()
{
    cout << "Volani destruktoru pro kostku s " << pocet_sten << " stenami" << endl;
}

int Kostka::hod()
{
    return rand() % pocet_sten + 1;
}

Nyní kostka vždy vygeneruje jiná čísla a tím jsme hotovi.

Konstruktor pro správu paměti

Druhým případem, kdy můžeme použít konstruktor (a destruktor), je pro správu paměti. Díky tomu, že se konstruktory a destruktory volají automaticky, máme jistotu, že se kód vždy provede. Můžeme si tedy v konstruktoru alokovat paměť a v destruktoru ji zase smazat. Pro příklad si vezmeme naši arénu, ve které jsou aktuálně dva bojovníci. Řekněme, že chceme zadat počet bojovníků v parametru - musíme si dynamicky vytvořit pole bojovníků. Soubor Arena.h upravíme následovně:

#ifndef __ARENA_H_
#define __ARENA_H_
#include "Hrac.h"

class Arena
{
public:
    Hrac** hraci;
    int pocet_hracu;
    Arena(int _pocet_hracu); // byla změněn název parametru
    ~Arena();
};
#endif

Dvou hvězdiček se nebojte - je to pole ukazatelů na Hrac (nemůžeme vytvořit pouze pole hráčů, protože nemáme výchozí=bezpa­rametrický konstruktor, který je potřeba). V konstruktoru toto pole alokujeme, podle počtu hráčů se zeptáme na jména a hráče vytvoříme. V destruktoru poté provedeme opačnou operaci a vše smažeme. Na kód se můžete podívat:

Arena.cpp

#include <iostream>
#include "Arena.h"

using namespace std;

Arena::Arena(int _pocet_hracu)
{
    pocet_hracu = _pocet_hracu; // uložení počtu hráčů
    hraci = new Hrac*[pocet_hracu]; // vytvoření pole pro hráče
    for (int i = 0; i < pocet_hracu; i++)
    {
        string jmeno;
        cout << "Zadejte jmeno hrace: ";
        cin >> jmeno;
        hraci[i] = new Hrac(jmeno); // vytvoření hráče
    }
}

Arena::~Arena()
{
    for (int i = 0; i < pocet_hracu; i++)
        delete hraci[i]; // mazání hráčů
    delete[] hraci; // mazání pole
    hraci = NULL;
}

Pokud bychom paměť nemazali, zůstávala by alokována a neměli bychom se k ní jak dostat (neexistoval by na ni ukazatel) a nebylo by ji možné smazat tedy ani později. Pokud by probíhalo vytváření instancí například v cyklu, potom by program začal konzumovat stále více a více RAM, dokud by ji nezabral celou (a mít řádově gigabajty RAM pro takovou malou aplikaci je již poněkud zvláštní). Jestliže není volná RAM paměť a program požádá o další, operační systém už nemá co přidělit a aplikaci shodí. Proto, pokud se vám stává, že aplikace po nějaké době spadne, zkuste si zkontrolovat, kolik zabírá místa v paměti a pokud se toto místo neustále zvětšuje, zřejmě někde neuvolňujete paměť - tzv. memory leak.

main.cpp

Tím máme hotovou arénu a můžeme ji použít v main.cpp:

#include <iostream>
#include "Kostka.h"
#include "Arena.h"

using namespace std;


int main()
{
    Kostka kostka;
    for (int i = 0; i < 10; i++)
        cout << kostka.hod() << " ";
    cout << endl;

    Arena arena(4);
    cin.get();
    return 0;
}

Výsledek:

Konzolová aplikace
Volani konstruktoru s parametrem
Volani bezparametrickeho konstruktoru
2 6 1 2 1 6 2 3 1 4
Zadejte jmeno hrace: Pavel
Zadejte jmeno hrace: Karel
Zadejte jmeno hrace: Zdenek
Zadejte jmeno hrace: Lukas
Volani destruktoru pro kostku s 6 stenami

Vše funguje podle našich představ. Alokování a uvolňování paměti je nejčastější věc, která se v konstruktorech a destruktorech provádí, proto doporučuji si poslední ukázku pořádně projít a pochopit, jak to vlastně funguje.

To je pro tuto lekci vše a příště, Ukazatel this v C++, si odstraníme ty škaredé názvy parametrů začínajících podtržítkem. Zdrojové kódy z dnešní lekce jsou přiloženy pod článkem ke stažení jako vždy.


 

Stáhnout

Staženo 32x (7.21 kB)
Aplikace je včetně zdrojových kódů v jazyce C++

 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
3 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity (8)

 

 

Komentáře

Avatar
Ondřej Čoček:10. května 13:02

Dobrý den, chtěl bych jenom dodat, že destruktor pro první kostku se přece nevykoná tak jak máte uvedeno ve výpisu, jelikož k němu buď to nedojde vůbec po ukončení programu příkazem return 0; nebo se vykoná na pozadí po skončení programu, ne?
Děkuji

 
Odpovědět 10. května 13:02
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Ondřej Čoček
patrik.valkovic:10. května 14:18

Destruktor se volá vždy a to v okamžiku skončení platnosti proměnné. Například platnost parametrů funkce končí její zavírací složenou závorkou. Stejně je to tady. Tím, že se funkce ukončí, skončí platnost všech proměnných ve funkci definovaných a jsou destruovány.

Odpovědět 10. května 14:18
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.