Kurz C++ (10.)
Z předchozího
článku máme hrubý nástin objektového programování. V dnešním díle se budeme
věnovat speciálním metodám - konstruktoru a destruktoru a podíváme se podrobněji
na členské funkce. Probereme
také vytváření objektů a to jak statické, tak dynamické.
10.1. Základy práce s objekty
V uvedeném kódu jsou
vynechány direktivy #include. Většina příkladů vyžaduje
stdio.h kvůli
konstantě NULL a jiným, iostream.h kvůli
cout a string.h kvůli
manipulaci s řetězci. Pro pokusy s funkcí malloc() je nutné použít
hlavičkový soubor malloc.h.
Nejprve se
zaměříme na zmíněný konstruktor. Konstruktor je členská metoda třídy, která je
volána jako první při vytváření objektu, přičemž nezáleží na tom, zdali
vytváříme objekt staticky nebo dynamicky (viz. níže). Má tedy na starost inicializaci
daného objektu (např. alokaci paměti, nastavení určitých proměnných atd.). Aby
se metoda stala konstruktorem stačí jediné a to nazvat ji stejným jménem jako
třídu a vynechat návratovou hodnotu, protože konstruktory nikdy nic nevrací.
Tedy pro třídu Clovek bude metoda s názvem
Clovek() konstruktorem. Pokud se
podíváte na příklady z minulé lekce, tak si jistě všimnete, že ve třídách
konstruktor chybí. To je ale v pořádku, jestliže totiž konstruktor explicitně
nedeklarujete, dosadí si překladač takzvaný implicitní konstruktor, což je
vlastně prázdná funkce. V případě, že je to vhodné lze samozřejmě napsat více
konstruktorů, které budou mít rozdílné parametry nebo různý počet parametrů,
překladač si pak vybere ten správný právě podle parametrů, které mu předáte.
Teď, když víte základní informace o
konstruktoru, je asi jasné k čemu bude dobrý destruktor. Je to tedy opět
metoda třídy, která je zodpovědná za zrušení objektu (tedy např. uvolnění
paměti alokované v konstruktoru). Destruktor se vyznačuje opět stejným jménem
jako třída, ale navíc má předsazen znak "~". Opět nevrací žádnou hodnotu a
navíc musí být bezparametrický. Každá třída může mít pouze jeden destruktor.
Nyní si uvedeme krátkou ukázku
konstruktoru a destruktoru. Pro následující jednoduché a krátké třídy použiji
zápis metod přímo v definici třídy. Pro rozsáhlejší třídy je však vhodnější
zapsat delší členské funkce vně tuto definici, jak bylo uvedeno v minulé
lekci. Ale zpět k naší ukázkové třídě :
class
Buffer {
private :
BYTE *m_Buffer;
// BYTE je definovan jako typedef unsigned char BYTE;
public :
Buffer(int _velikost) { m_Buffer =
new BYTE[_velikost]; }
~Buffer() { delete[] Buffer; }
void ZapisNa(int _pozice, BYTE _hodnota) { m_Buffer[_pozice] = _hodnota; }
};
Tento kód vypadá na první pohled celkem
rozumně, ale co by se stalo, kdyby náhodou nebylo možné přidělit paměť o
velikosti _velikost? Z konstruktoru nelze vrátit návratovou hodnotu
indikující chybu. Proto bychom měli nejprve nastavit proměnnou
m_Buffer
(členské proměnné je lepší pro přehlednost značit předponou
m_, která nám
zkracuje member variable. Pro zápis parametrů pak používám
_ a za ním jméno
parametru. V těle procedury pak jasně vidím, s kterou proměnnou pracuji) na předem dohodnutou hodnotu, v tomto případě tedy
nejspíše NULL a v destruktoru a členských metodách pak kontrolovat, zdali je
m_Buffer platný (tedy ne-NULLový). Jinak bychom voláním funkce
ZapisNa()
způsobili chybný přístup do paměti. Přepsáno do kódu tedy :
class
Buffer {
private :
BYTE *m_Buffer;
public :
Buffer(int
_velikost) { m_Buffer =
new BYTE[_velikost]; }
~Buffer() { if(m_Buffer) { delete[] m_Buffer; m_Buffer = NULL; } }
void ZapisNa(int _pozice, BYTE _hodnota) { if (m_Buffer) { m_Buffer[_pozice] =
_hodnota; } }
};
Jinou možností by pak bylo využívat
konstruktor jen k nastavení proměnných a vytvořit členskou funkci
Init(int
_velikost), která by nám vracela např. TRUE v případě úspěchu a FALSE v
případě neúspěchu. Dále bychom vytvořili jednu soukromou (private) proměnnou
m_JeInit typu boolean. Tuto proměnnou bychom v případě úspěšného provedení
funkce Init() nastavili na TRUE, jinak na FALSE. Pak bychom ji testovali v každé
metodě, jestliže bude TRUE, pak víme, že pole BYTEů má přidělenou paměť a
přístup do něj tedy nezpůsobí chybný přístup do paměti. Opět si ukážeme kód :
class
Buffer {
private :
BYTE *m_Buffer;
BOOL m_JeInit;
public :
Buffer() { m_Buffer = NULL;
m_JeInit = FALSE; }
~Buffer() { if(m_Buffer) { if(m_JeInit) { delete[] m_Buffer; m_Buffer = NULL;
} }
BOOL Init(int _velikost) { m_Buffer = new BYTE[_velikost]; if(m_Buffer) {
m_JeInit = TRUE; } return m_JeInit; }
void ZapisNa(int _pozice, BYTE _hodnota) { if (m_JeInit) { m_Buffer[_pozice] =
_hodnota; } }
};
Mně osobně se zamlouvá více první způsob,
ale záleží jen na vás, který si vyberete. V dalším kódu budu používat způsob
první. V výše uvedeném příkladu se skrývá ještě jedna vada na kráse a to, že neověřujeme
meze v členské funkci ZapisNa(), je tedy možné zapsat mimo alokované pole, což
určitě není to nejlepší. Tento problém lze ale jednoduše vyřešit a
to přidáním další členské soukromé proměnné
m_Velikost, kterou nastavíme po
úspěšném přidělení paměti a pak ve funkci
ZápisNa() ověříme, jestli jsme náhodou
nepřekročili mez.
Ještě se podíváme na takzvanou
inicializační část, kterou je možno použít u konstruktorů, zavedeme si novou
třídu Clovek, která bude mít jako datové prvky proměnné
m_Vek a m_Vyska:
class Clovek {
private :
BYTE m_Vek; int m_Vyska;
//
m_Vek jsem dal jako BYTE z duvodu usetreni pameti,
// protoze cloveka starsiho
nez 255 let asi tezko potkate ;))
public :
Clovek(BYTE _Vek, int _Vyska) : m_Vek(_Vek),
m_Vyska(_Vyska) { ; }
};
Vidíme, že konstruktor
Clovek() nemá žádné
tělo. Členské proměnné nastavíme pomocí inicializační části, která se píše
hned za uzavírající závorku argumentů, oddělíme ji ":" a pak napíšeme
proměnnou, do které chceme přiřadit a za ni do závorky hodnotu, kterou jí
chceme přiřadit. Výsledný efekt bude stejný, jako kdybychom do těla
konstruktoru napsali následující příkazy:
m_Vek = _Vek; m_Vyska = _Vyska;
Je pěkné, že sice víte k čemu je
konstruktor a destruktor, ale abychom mohli třídu použít a vytvořit její
instanci, musíme vědět jak toho v programu dosáhnout. Například
celočíselnou hodnotu (int) můžeme alokovat buď staticky nebo dynamicky.
Nejprve se podívejme na statickou, kterou jsme používali od začátku, napíšeme
jméno proměnné (int, real a jméno proměnné). Překladač tak přesně ví, kolik
paměti po něm chceme a tak ji pro nás alokuje. Dynamickou alokaci musíme
použít tam, kde předem nevíme, jak velká data budou. Třídu Buffer
můžeme chtít použít třeba jen pro 5 bytů, ale také třeba pro několik desítek
megabyte. Tato
paměť se alokuje za běhu programu pomocí funkce malloc() (v klasickém C) nebo
pomocí operátoru new (v C++). To samé
lze provést s třídami. Nejprve ukáži nějaký kód, např. funkci
main() ve které
budeme chtít vytvořit instance třídy Buffer:
void main(void)
{
Buffer mujStatickyBuffer(10);
// Tedy statická alokace
Buffer *mujDynamickyBuffer = new
Buffer(15); // Dynamická alokace
// Ted provedeme nejake operace s
bufferem
mujStatickyBuffer.ZapisNa(5, 5);
mujDynamickyBuffer->ZapisNa(10,8);
// Pred skoncenim musime dynamicke
promenne zrusit
delete(mujDynamickyBuffer);
}
Uvedený kód nám tedy vytvoří jeden
statický a jeden dynamický objekt. Je mezi nimi několik rozdílů a to v
přístupu k členským funkcím, u statického používáme tečkové notace, kdežto u
dynamického musíme použít symbolu ->, protože je to vlastně ukazatel. Další
rozdíl je v tom, že staticky vytvořené proměnné se po výstupu z bloku nebo
funkce samy zruší. Naopak proměnné, které si vytvoříme dynamicky musíme sami
explicitně uvolnit, jinak dojde ke ztrátě paměti (tzv. MEMORY
LEAK). Liší se také umístění těchto paměťových bloků v paměti. Pokud vytvoříme
globální proměnnou, pak je uložena v tzv. datovém segmentu programu, kde se
nachází po celou dobu chodu programu. Lokální proměnné, vytvořené v těle
nějaké funkce nebo bloku (uzavřeného mezi složené závorky) jsou pak uloženy na
zásobníku (STACK) a jsou zrušeny při výstupu z této funkce nebo bloku. A
konečně pokud si zažádáme o paměť pomocí malloc() nebo new je
vymezena paměť na hromadě (HEAP), doba její existence závisí jen na tom, kdy
se rozhodneme vrátit ji systému. Uveďme si tedy ještě kratičký příklad na
obecné alokace:
void main(void)
{
// Nejdrive se
na to podivame v C
int
i;
// Tedy statická alokace, proměnná
bude na stacku
int *p_i
= NULL; //
Pointer na int a nastavíme na NULL, abychom mohli otestovat
jestli byla alokace úspěšná
p_i
= (int *)malloc(1*sizeof(int)); // Alokujeme 1 velikost integeru. Musíme
explicitně přetypovat, jinak si překladač bude stěžovat
if(p_i)
{ // Vždy raději ověřujte, jestli je proměnná platná
*p_i = 5; // dáme na místo kam ukazuje p_i číslo 5
// Nějaké operace s integerem na adrese p_i
// ....
// ....
// Uvolnění paměti funkce free()
free(p_i);
}
// Ted
to same v C++
//
To same lze zapsat pomoci new
p_i
= NULL;
p_i
= new int; // Zde nemusíte
ukazatel přetypovávat
if(p_i) {
*p_i = 6;
// Nějaké operace s integerem na adrese p_i
// ....
// ....
// Uvolnění paměti operátoru delete
delete p_i;
}
}
Nyní opustíme téma konstruktorů a
destruktorů a podíváme se na členské metody, o nichž toho zatím mnoho nevíme.
Metod je více druhů. Nejprve se zmíním o tzv. metodách nastav/vrať (set/get).
Již víme, že v objektovém programování využíváme výhod zapouzdření, tedy
neviditelnosti datových členů z vnějšího prostředí. Proto pro většinu
datových členů naprogramujeme jednoduché členské funkce, které
nastavují/vrací jejich hodnotu. Tyto funkce jsou většinou tak kratičké, že je
lze umístit přímo do deklarace třídy. Vytvoříme si novou ukázkovou třídu
Clovek:
class Clovek {
private : BYTE m_Vek;
int m_Vyska;
public : Clovek(BYTE _Vek, int _Vyska) : m_Vek(_Vek),
m_Vyska(_Vyska) { ; }
void nastavVysku(int _NovaVyska) { m_Vyska = _NovaVyska; }
int vratVysku() { return m_Vyska; }
void nastavVek(BYTE _NovyVek) { m_Vek = _NovyVek; }
BYTE vratVek() { return m_Vek; }
};
Pro kratičké funkce je vhodné uvést
modifikátor inline, který překladači říká, že bychom byli rádi, kdyby místo
volání této funkce nahradil její výskyt přímo tělem této funkce. Překladač nás
může, ale nemusí poslechnout. Důvodem této maličkosti je jisté uspoření času,
které zabere volání funkce, musí se totiž uložit vzhledem k velikosti této
funkce hodně informací (kam se vrátit, stav registrů, atp.). Inline funkce je
vlastně makro, které nám ale navíc nabízí typovou kontrolu. Další modifikátor,
který můžeme použít je modifikátor const, který uvedeme za ukončující závorku
argumentů. Tento modifikátor říká překladači, že daná funkce nemodifikuje
objekt, pro který byla volána. Tedy const je vhodné uvést u všech metod, které
jen vrací hodnoty, ale pro metody které nastavují hodnoty jej nelze použít.
Tedy např. funkce vratVysku() by mohla vypadat takto :
inline vratVysku() const { return m_Vyska; }
Další metody se kterými se setkáme jsou
operace nad objektem, může to být například procedura, která vypíše pro nás
významné datové členy. Dnešní lekci zakončíme trošku ucelenějším programem.
Ukážeme si také zápis metody mimo deklaraci třídy a rozšíříme našeho člověka o
jméno:
#include <stdio.h>
#include <iostream.h>
#include <string.h>
typedef unsigned char BYTE;
class Clovek {
private :
BYTE m_Vek;
int m_Vyska;
char *m_Jmeno;
public :
Clovek(char *Jmeno, BYTE _Vek, int _Vyska);
~Clovek() { if(m_Jmeno) { delete[] m_Jmeno; } }
void Starni(BYTE _OKolik) { m_Vek += _OKolik; }
void nastavVysku(int _NovaVyska) { m_Vyska = _NovaVyska; }
int vratVysku() const { return m_Vyska; }
void nastavVek(BYTE _NovyVek) { m_Vek = _NovyVek; }
BYTE vratVek() const { return m_Vek; }
void Vypis();
};
Clovek::Clovek(char *Jmeno, BYTE _Vek, int _Vyska) :
m_Vek(_Vek), m_Vyska(_Vyska)
{
m_Jmeno = NULL;
if(Jmeno) {
int delka =
strlen(Jmeno) + 1; // Kvuli poslednimu '\0'
m_Jmeno = new
char[delka];
if(m_Jmeno) {
strncpy(m_Jmeno, Jmeno, delka);
}
}
}
void Clovek::Vypis()
{
if(m_Jmeno) { cout << "Jmeno : " <<
m_Jmeno << '\n'; }
else { cout << "Bezejmenny" << '\n'; }
cout << "Vek : " << (int)m_Vek << '\n';
cout << "Vyska : " << m_Vyska << '\n';
}
int main(int argc, char* argv[])
{
Clovek muj("Pepik Frantik",10,158);
muj.Vypis();
muj.Starni(5);
muj.Vypis();
char c;
cin >> c; // Cekame na stisk klavesy
return 0;
}
Budete-li mít chuť, můžete si
doprogramovat členské funkce nastavJmeno(),
vratJmeno() a třeba ještě funkci
Povyrost(). V příštím díle si povíme o kopírovacím konstruktoru,
přetěžování členských funkcí, o statických členských proměnných a
metodách,
jak vytvářet přehledné moduly (jak dělit třídy do souborů) a
pravděpodobně začneme s
dědičností.
Příště nashledanou.
|