V dnešním pokračování se ještě vrátíme k přetěžování operátorů a to konkrétně k operátorům new a delete. Potom se vrátíme k tomu, co jsme načali minule, tedy dědičnosti. Povíme si něco o jednoduché dědičnosti a vše si ukážeme na příkladech.
Jak již víme operátory new a delete slouží k alokaci a uvolnění dynamicky alokované paměti. Oba operátory existují ve dvou verzích a to operátor pro jednoduché proměnné:
void* operator new(size_t _velikost);
a pak je tu operátor, který je určen pro alokace polí:
void* operator new[](size_t _velikost);
Použitím operátoru new dojde právě na základě druhu proměnné, pro kterou chceme alokovat paměť k zavolání jedné z těchto funkcí. Operátor delete má také dvě verze:
void operator delete(void *ptr);
, resp. pro pole:
void operator delete[](void *p);
Je vhodné poznamenat, že operátor new se stará pouze o alokaci paměti, tedy např. volání konstruktoru je starostí překladače. Stejně tak i volání destruktoru v případě operátoru delete. Jak již víme, operátor new v případě, že není dostatek paměti pro uspokojení požadavku, vrací NULL. Jinou reakcí na nedostatek paměti může být vyvolání výjimky, ale těmi se budeme zabývat až v některém z následujících dílů.
Narozdíl od ostatních operátorů lze tyto dva operátory kromě přetížení i předefinovat, čímž dojde k nahrazení standardních verzí po celou dobu běhu programu. Jedním z důvodů pro předefinování těchto operátorů může být potřeba ověření, zdali správně uvolňujeme paměť nebo inicializace bloku paměti předem zadanou hodnotou. Mohli bychom také v operátorové funkci new zvětšit o malý kousek alokovaný úsek, který bychom vyplnili předem zadanou hodnotou a při uvolňování bychom tento úsek mohli zkontrolovat. Pokud by byl změněn, pak pravděpodobně došlo k nechtěnému přístupu do paměti, která nám už nepatří. V ukázce bude program při provedení alokace vypisovat kolik byte paměti si uživatel přeje přidělit a při uvolnění pak vypíše adresu bloku paměti, který se uvolňuje:
#include "stdafx.h"
// Pouzivame predkompilovane hlavicky
#include <stdio.h>
#include <malloc.h>
#include <iostream.h>
void* operator new(size_t _velikost)
{
void* p = NULL;
printf("Pokus o alokaci %u byte ... ", _velikost);
if(!_velikost) { _velikost = 1; } // operator new by mel vracet ruzne
adresy
p = malloc(_velikost);
if(p)
{
printf("uspesne (adresa : %p)\n", p);
}
else
{
printf("neuspesne\n");
}
return p;
}
void operator delete(void* _ptr)
{
printf("Dealokace bloku na adrese %p\n", _ptr);
if(_ptr)
{
free(_ptr);
}
}
int main(int argc,
char* argv[])
{
int i = 0;
int *p_int;
p_int = new int;
*p_int = i;
int *p_int2;
p_int2 = new int[4096];
delete p_int2;
delete p_int;
char c;
cin >> c;
return 0;
}
Zdrojový kód naleznete v sekci Downloads (projekt PretNew).
U standardního operátoru new je v dokumentaci uvedeno, že v případě opakované alokace 0 byte operátor new vrací ukazatele na různé oblasti paměti. To máme nyní zajištěno alokací 1 byte. Tedy i když uživatel chce (spíše ale nechce) blok paměti, vrátíme mu jeden byte.
Pokud se rozhodneme, že se nám funkce printf() nelíbí a nahradíme ji voláním cout dostaneme se do potíží. Tento objekt totiž interně používá operátory new a delete k alokování a uvolnění bloku pomocné paměti. Avšak námi definovaný operátor nahradil standardní verzi, čímž dojde k rekurzivnímu volání funkce, které ale chybí ukončovací podmínka. Tím může u některých implementací vzniknout nekonečný cyklus.
Kvůli výše uvedenému se tedy redefinici operátorů new a delete na globální úrovni snažíme vyhnout. Použijeme tedy možnosti přetížení operátoru new jako metody nějaké třídy nebo jako globálního operátoru s přidanými parametry, kdy se můžeme rozhodnout jestli použijeme naší verzi nebo standardní:
void* operator new(size_t
_velikost, int _iMuj)
{
printf("Toto je muj operator new");
return operator new(_velikost);
}
int main(int argc, char* argv[])
{
int *p_int;
p_int = new(5) int; // Dojde k zavolani naseho operatoru new
delete p_int;
return 0;
}
Zdrojový kód naleznete v sekci Downloads (projekt PretNew2).
Podobně jako jsme přetěžovali operátory new a delete bychom mohli přetížit jejich verze pro pole, operátory new[] a delete[]. Ve většině případů je to však zbytečné, neboť tyto operátory interně používají operátorů new a delete.
Nyní si ukážeme jak přetížit operátor pro třídu:
#include "stdafx.h"
// Pouzivame predkompilovane hlavicky
#include <stdlib.h>
#include <memory.h>
#include <iostream.h>
class Test {
public:
void *operator new(size_t _velikost, char _cznak);
char VratZnak() { return m_cZnak; }
private:
char m_cZnak;
};
void* Test::operator new(size_t _velikost, char _cZnak)
{
void *p = malloc(_velikost);
if(p)
{
memset(p, _cZnak, _velikost);
}
return p;
}
int main(int argc, char* argv[])
{
Test *cTest = new('G') Test;
cout << "Znak je : " << cTest->VratZnak() << endl;
char c;
cin >> c;
return 0;
}
Při alokaci paměti celý úsek nastavíme na zadanou hodnotu. Globální operátor new je před touto třídou skryt, takže příkaz:
Test *cTest = new Test;
vyvolá chybu při překladu. Pokud bychom ovšem potřebovali pro alokaci použít standardního operátoru new, pomůže nám následující řádek:
Test *cTest = ::new Test;
Přetížíme-li operátory new a delete jako metody objektového typu, jsou vždy statické. A to i v případě, že klíčové slovo static neuvedeme.
Zdrojový kód naleznete v sekci Downloads (projekt PretNew3).
Ještě se podíváme na zvláštní operátor new, který je v souboru new.h. Je definován takto:
void* operator new(size_t
_velikost, void* p)
{
return p;
}
Jak vidíme, tak tento operátor ve svém těle neobsahuje žádné příkazy pro alokaci bloku paměti. K čemu se tedy vlastně hodí? Podívejme se na následující příklad:
#include "stdafx.h"
// Pouzivame predkompilovane hlavicky
#include <iostream.h>
void* operator new(size_t _velikost, void *_p)
{
return _p;
}
void operator delete(void *_ptr, void *_p2)
{
;
}
class Test
{
private:
char a, b;
public:
Test() { a = 'A'; b = 'B'; }
};
char pole[64];
int main(int argc, char* argv[])
{
// Zinicializujeme na X
for(int i = 0; i < 64; i++)
{
pole[i] = 'X';
}
Test *mujtest = new(pole) Test;
cout << "Adresa pole je : " << &pole << endl;
cout << "Adresa objektu je : " << mujtest << endl;
for(i = 0; i < 64; i++)
{
cout << pole[i];
}
char c;
cin >> c;
return 0;
}
Zdrojový kód naleznete v sekci Downloads (projekt PretNew4).
Na výstupu programu pak uvidíme, že se adresy pro pole znaků a instanci třídy Test shodují. Takto definovaný operátor tedy jen vrátí ukazatel na zadanou adresu. Pokud si pak vypíšeme obsah pole, uvidíme, že pole ve svých dvou prvních bytech obsahuje členské proměnné třídy Test. Prázdný operátor delete je uveden, aby překladač nehlásil upozornění, že neexistuje odpovídající operátor delete, který by uvolnil paměť pokud by vznikla při inicializaci výjimka. Pokud chceme definovat odpovídající operátor delete k operátoru new, pak musí mít parametry shodné od druhého a případně výše. Tento operátor nemusíme vlastnoručně přetěžovat, stačí pouze vložit soubor new.h do našeho programu.
Jiným příkladem by mohlo být přetížení operátoru new jako metody třídy. V programu bychom pak definovali úsek paměti (pole) pro vytváření instancí této třídy. Toto pole by mohlo být také statickou členskou proměnnou dané třídy. Operátor new by pak vracel ukazatele do tohoto pole a v členské statické proměnné by si udržoval index na zbývající volné místo v tomto poli. Pokud bychom v tomto poli chtěli ukládat i pole objektů, tedy pomocí operátoru new[], museli bychom ho v této třídě přetížit. Jinak by se použil globální operátor, který by alokoval blok paměti z dostupné volné paměti. Pro alokování paměti pro třídu Test mimo pole (tedy ve volné paměti) lze opět použít globální operátor new, který musíme kvalifikovat čtyřtečkou (::).
A konečně, chceme-li jen alokovat paměť (bez zavolání konstruktoru) nebo paměť uvolnit (bez volání destruktoru):
Test *bezvolani = (Test *)operator new(sizeof(Test));
// Pouziti
operator delete(bezvolani);
Zdrojový kód naleznete v sekci Downloads (projekt PretNew5).
V minulém díle jsme se dověděli nějaké obecné informace o dědičnosti. Dnes si ukážeme jak použít jednoduché dědičnosti. Začneme ale pojmem hierarchie tříd. Složitější programy se neobejdou pouze s jednou třídou, mohou jich mít stovky. Při použití dědičnosti vzniknou příbuzenské vztahy mezi některými třídami. Tuto strukturu nazýváme hierarchií tříd. Pro minule uvedený příklad se zoologickou zahradou by mohl vypadat například takto:
Vidíme, že hierarchie pro jednoduchou dědičnost má tvar stromu. Libovolná třída, kromě základní, má pouze jednoho předka. Třídu na nejvyšší úrovni nazýváme kořenovou třídu (angl. root), je to společný prvek pro všechny třídy v hierarchii. Třída ze které vede čára je třídou odvozenou, třída v níž končí šipka je třídou základní. Směr šipky je zvolen tímto způsobem, protože odvozená třída zná svou základní třídu, kdežto základní třída neví, které další třídy z ní budou odvozeny. Jazyk C++ nám kromě jednoduché dědičnosti umožňuje použít dědičnost vícenásobnou a dědičnost opakovanou. Při vícenásobné dědičnosti má třída více rodičů, v grafu tedy bude ukazovat na více tříd v hierarchii. Při opakované dědičnosti, ke které dochází především ve složitějších hierarchiích, zdědí třída vlastnosti některého předka více cestami.
Mějme třídu nebo strukturu Predek. Potom třídu nebo strukturu Potomek, odvozenou od třídy Predek, deklarujeme následovně:
class Potomek : Predek {
/*Pridane prvky*/
};
nebo
struct Potomek : Predek {
/*Pridane prvky*/
};
V C++ je struktura vlastně to samé co třída, s tím rozdílem, že všechny její prvky jsou implicitně veřejné (public). V obou případech můžeme před jménem třídy, od které dědíme uvést specifikátory přístupu (public, private, protected). Pokud neuvedeme žádný z těchto specifikátorů platí následující: pokud je děděný objekt třídou platí implicitně private, u struktur public. Následující tabulka ukazuje, jak budou ovlivněna přístupová práva ve zděděné třídě:
přístupové právo v rodičovském objektu |
public |
protected |
private |
odvození public |
public |
protected |
nepřístupný |
odvození protected |
protected |
protected |
nepřístupný |
odvození private |
private |
private |
nepřístupný |
Že je prvek nepřístupný znamená, že k němu není možno přistupovat v těle metod přidaných do odvozené třídy. K přístupu k nim je tedy nutné použít členských metod, jako bychom k nim přistupovali z vnějšku třídy. Chráněné prvky můžeme použít v nově přidaných metodách odvozené třídy, ale nejsou přístupné z venku. Veřejné prvky lze opět využít v nově přidaných metodách odvozené třídy, ale kromě toho jsou ještě přístupné vnějším přístupem.
Víme-li, že od dané třídy budeme chtít dědit a v přidaných metodách pak přistupovat k členským proměnným základní třídy, je nejlepší použít pro proměnné v základní třídě přístupového práva protected a třídu zdědit pomocí specifikátoru přístupu public. Vyhneme se tím zbytečnému volání metod zpřístupňujících dané proměnné a zachováme pravidlo, že proměnné nebudou přístupné zvnějšku objektu. V příkladech věnovaných dědičnosti budeme většinou používat právě tohoto způsobu.
Pokud chceme, můžeme u vybraných proměnných vrátit jejich přístupová práva na úroveň kterou měla ve třídě od níž odvozujeme. Ukázka:
class Predek {
public:
char a;
};
class Potomek : Predek {
public:
Predek::a; // Pokud neni uvedeno, prekladac
pri primem pristupu k prvku a ohlasi chybu (public se totiz pri
private dedeni
stane private)
};
To samé lze zařídit pomocí novějšího zápisu:
class Potomek : Predek {
public:
using Predek::a;
};
Pro změnění přístupového práva metody je nutné uvést v obou případech metodu bez závorek.
Pokud se nám nezdá vhodné ponechat přístup k nějaké metodě rodičovské třídy, lze ji zakázat. Provedeme to tak, že v odvozené třídě deklarujeme soukromou metodu se stejným typem a parametry. Tato metoda původní metodu zastíní a tím znemožní její použití vně třídy. V případě, že bychom ji chtěli zavolat uvnitř nějaké členské funkce musíme ji zavolat jejím celým jménem, tedy např. Predek::Metoda().
Existují také prvky, které se nedědí. Nedědí se konstruktor. Jeho volání zařídí překladač, popřípadě ho lze v konstruktoru odvozené třídy zavolat explicitně. Podobně se nedědí destruktor, ale je opět vyvolán překladačem. A také se nedědí přetížený operátor operator=(). Pokud není explicitně definován pro odvozenou třídu, využije se operátor předka při konstrukci implicitního přiřazovacího operátoru potomka.
Příště se podíváme podrobněji na volání konstruktorů a destruktorů v odvozených třídách. Dále virtuálnímu dědění, virtuálním metodám a abstraktním třídám.
Příště nashledanou.