Lekce 4 - Destruktory a aplikace konstruktorů v C++
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á.
#include <iostream> #include "Kostka.h" using namespace std; 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 hod(); 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.
Pozn.: Vedle knihovny cstdlib
musíme includovat i knihovnu
ctime
.
#include <iostream> #include "Kostka.h" using namespace std; //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); // byl 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í=bezparametrický 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.
V následujícím cvičení, Řešené úlohy k 3. a 4. lekci OOP v C++, si procvičíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 108x (7.21 kB)
Aplikace je včetně zdrojových kódů v jazyce C++