-
V této kapitole budeme vyvíjet složitější konzolovou aplikaci. Jedná se
o jednoduchou databázi, ve které si ukážeme komplexnější použití virtuálních
funkcí. Nejprve vytvoříme deklaraci a implementaci třídy osoba.
Tato třída bude obsahovat dvě chráněné datové složky a jednu virtuální
metodu.
class osoba {
protected:
char jmeno[25];
int plat;
public:
virtual void
zobraz(void);
};
void osoba::zobraz(void){
cout <<
"osoba::zobraz - chybějící metoda v odvozené třídě\n";
}
Implementace této třídy je snadná, bude obsahovat pouze implementaci
virtuální metody zobraz. Tato třída bude základem pro odvozování
dalších specializovaných tříd, ale nikdy ji nebudeme používat (budeme ji
používat pouze jako třídu předka). Z tohoto důvodu by metoda zobraz
neměla být nikdy volána a do její implementace vložíme pouze výpis signalizace
chyby.
Dále vytvoříme tři odvozené třídy. Jsou to třídy vedouci, programator
a sekretarka. Všechny obsahují metodu zobraz, která je stejná
jako tato metoda v třídě předka. Jsou to tedy virtuální metody.
class vedouci : public
osoba {
char titul[25];
public:
void inicializace(char
jm[], int pl, char ti[]);
void zobraz(void);
};
class programator
: public osoba {
char titul[25];
char jazyk[25];
public:
void inicializace(char
jm[], int pl, char ti[], char ja[]);
void zobraz(void);
};
class sekretarka
: public osoba {
char tesnopis;
int rychlost_psani;
public:
void inicializace(char
jm[], int pl, char te, int ry);
void zobraz(void);
};
void vedouci::inicializace(char
jm[], int pl, char ti[]){
strcpy(jmeno,
jm);
plat = pl;
strcpy(titul,
ti);
}
void vedouci::zobraz(void){
cout <<
"Vedoucí --> " << jmeno << " má plat
" << plat <<
" a je " << titul << ".\n\n";
}
void programator::inicializace(char
jm[], int pl, char ti[], char ja[]){
strcpy(jmeno,
jm);
plat = pl;
strcpy(titul,
ti);
strcpy(jazyk,
ja);
}
void programator::zobraz(void){
cout <<
"Programátor --> " << jmeno << " má plat " << plat <<
" a je " << titul << ".\n\n";
cout <<
"
" << jmeno<<" programuje v "<<jazyk<<" .\n\n";
}
void sekretarka::inicializace(char
jm[], int pl, char te, int ry){
strcpy(jmeno,
jm);
plat = pl;
tesnopis =
te;
rychlost_psani
= ry;
}
void sekretarka::zobraz(void){
cout <<
"Sekretářka --> " << jmeno << " má plat " << plat
<< ".\n\n";
cout <<
"
" << jmeno << " píše rychlostí " <<
rychlost_psani
<< " znaků za minutu a ";
if (!tesnopis)
cout << "ne";
cout <<
"ovládá těsnopis.\n\n";
}
V každé z těchto tříd nalezneme metodu inicializace, která provádí
inicializaci datových složek třídy a virtuální metodu zobraz vypisující
data třídy a to pro každou třídu jiným způsobem.
Vytvořené třídy nyní použijeme v jednoduchém programu. Na začátku programu
je deklarace pole ukazatelů o 10 prvcích, typu ukazatel na osoba.
Do tohoto pole budeme ukládat ukazatele na odvozené třídy. V programu alokujeme
několik objektů odvozených tříd, inicializujeme je a ukazatele na ně vložíme
do našeho pole. Na závěr programu informace uložené v objektech vypíšeme.
Program si prostudujte a vyzkoušejte.
osoba *stav[10];
int main(int argc,
char **argv)
{
vedouci *ved;
programator
*prog;
sekretarka
*sekr;
ved = new
vedouci;
ved->inicializace("Karel
Velký", 27000, "prezident");
stav[0] =
ved;
prog = new
programator;
prog->inicializace("Jan
Novák", 21000, "analytik", "Pascal");
stav[1] =
prog;
prog = new
programator;
prog->inicializace("Jiří
Novotný", 17000, "programaror", "C++");
stav[2] =
prog;
sekr = new
sekretarka;
sekr->inicializace("Marie
Hezká", 10370, 1, 350);
stav[3] =
sekr;
ved = new
vedouci;
ved->inicializace("Jarmila
Stará", 17000, "vedoucí účetní");
stav[4] =
ved;
prog = new
programator;
prog->inicializace("Jan
Kluzký",11000,"pomocný programator", "Pascal");
stav[5] =
prog;
for (int index
= 0; index < 6; index++)
stav[index]->zobraz();
return 0;
}
-
Metodu zobraz ve třídě osoba můžeme deklarovat také jako
čirou virtuální metodu. Čirá virtuální metoda nemá implementaci a nezamýšlíme
ji volat. Deklarujeme ji konstrukcí prototyp_metody
= 0; v našem případě jde o deklaraci
virtual void zobraz(void)
= 0;
Třída, která obsahuje alespoň jednu čirou metodu se v terminologii
C++ označuje jako abstraktní. Překladač nedovoluje definovat instance abstraktních
tříd. Čiré metody nemají definiční deklaraci. Pokus o volání čiré metody
skončí chybou. Změňte třídu osoba na abstraktní a vyzkoušejte.
-
V tomto zadání budeme pokračovat ve vývoji databáze zaměstnanců. Následuje
deklarace a implementace dalších dvou tříd, které využijeme k vytvoření
spojového seznamu zaměstnanců. Povšimněte si, že prvky spojového seznamu
neobsahují data, ale ukazatele na třídu osoba, kterou jsme vytvořili
v předchozím programu, a můžeme tedy vytvořit spojový seznam prvků tříd
osoba
a to bez nutnosti modifikace této třídy. V tomto souboru si povšimněte
použití dopředné deklarace třídy seznam_zamest (musíme ji použít,
neboť obě třídy se odkazují na sebe navzájem). Dále si povšimněte posledního
řádku v deklaraci třídy prvek_seznamu (friend
class seznam_zamest;), který dává třídě seznam_zamest
volný přístup k položkám třídy prvek_seznamu. Je to nutné, protože
metoda pridej_osobu musí přistupovat k položce dalsi.
class seznam_zamest;
// Dopředná deklarace
class prvek_seznamu
{
// Jeden prvek zřetězeného seznamu
osoba *data;
prvek_seznamu
*dalsi;
public:
prvek_seznamu(osoba
*novy_zamest){
dalsi = NULL;
data = novy_zamest;
};
friend class
seznam_zamest;
};
class seznam_zamest{
// Zřetězený seznam
prvek_seznamu
*zacatek;
prvek_seznamu
*konec;
public:
seznam_zamest()
{zacatek = NULL;}
void pridej_osobu(osoba
*novy_zamest);
void zobraz_seznam(void);
};
void seznam_zamest::pridej_osobu(osoba
*novy_zamest){
prvek_seznamu
*pom;
pom = new
prvek_seznamu(novy_zamest);
if (zacatek
== NULL)
zacatek = konec = pom;
else {
konec->dalsi = pom;
konec = pom;
}
}
void seznam_zamest::zobraz_seznam(void){
prvek_seznamu
*pom;
pom = zacatek;
do {
pom->data->zobraz();
pom = pom->dalsi;
} while (pom
!= NULL);
}
Databáze zaměstnanců je realizována pomocí spojového seznamu, který
vytváříme prostřednictvím našich dvou nových tříd. Program je jednoduchý
a nepotřebuje žádné vysvětlení. Prostudujte si jej a vyzkoušejte jej.
seznam_zamest seznam;
int main(int argc,
char **argv)
{
vedouci *ved;
programator
*prog;
sekretarka
*sekr;
ved = new
vedouci;
ved->inicializace("Karel
Velký", 27000, "prezident");
seznam.pridej_osobu(ved);
prog = new
programator;
prog->inicializace("Jan
Novák", 21000, "analytik", "Pascal");
seznam.pridej_osobu(prog);
prog = new
programator;
prog->inicializace("Jiří
Novotný", 17000, "programaror", "C++");
seznam.pridej_osobu(prog);
sekr = new
sekretarka;
sekr->inicializace("Marie
Hezká", 10370, 1, 350);
seznam.pridej_osobu(sekr);
ved = new
vedouci;
ved->inicializace("Jarmila
Stará", 17000, "vedoucí účetní");
seznam.pridej_osobu(ved);
prog = new
programator;
prog->inicializace("Jan
Kluzký",11000,"pomocný programator", "Pascal");
seznam.pridej_osobu(prog);
seznam.zobraz_seznam();
return 0;
}
-
Do našeho programu přidejte deklaraci a implementaci nové třídy nazvané
poradce
(přidané složky si zvolte) a přidejte příkazy na vyzkoušení použití nové
třídy.
-
V předchozí kapitole jsme se seznámili s vícenásobnou dědičností. Při vícenásobné
dědičnosti mohou ale vznikat problémy. Např. pro vstupy a výstupy v C++
používáme datové proudy. Základem datových proudů je třída ios.
Tato třída definuje vlastnosti společné všem datovým proudům (stavové příznaky,
formátovací příznaky apod.). Od třídy ios je odvozena řada specializovanějších
tříd. Jedná se také o třídy istream a ostream, které definují
vstupní a výstupní datové proudy. Společným potomkem těchto obou tříd je
třída iostream pro proudy, které umožňují zároveň vstup i výstup
dat. iostream tedy dědí vše od istream a ostream a
nic dalšího nepřidává. Z pravidel vícenásobného dědění vyplývá, že iostream
bude obsahovat dva zděděné podobjekty třídy ios a tedy i dvakrát
formátovací a stavové příznaky, což není vůbec vhodné. Jazyk C++ nabízí
řešení v podobě tzv. virtuálního dědění. To zajistí, že vícekrát zděděné
prvky se sloučí. Deklarujeme-li třídu ios jako virtuálního předka
tříd istream a ostream, bude jejich společný potomek iostream
obsahovat pouze jeden podobjekt typu ios.
Virtuálního předka vytvoříme tak, že při specifikaci předka použijeme
klíčové slovo virtual. Následuje příklad použití:
class a {
double x;
public:
a(){};
};
class aa : public
virtual a {
double a;
public:
aa() {};
};
class ab : public
virtual a {
double b;
public:
ab() {};
};
class X : public
aa, public ab {
double xx;
};
Instance třídy X bude nyní obsahovat pouze jeden podobjekt třídy
a.
Vyzkoušejte.
-
Jednou z výhod, které nám C++ nabízí, je možnost přetěžovat nejen funkce,
ale i převážnou většinu operátorů. Nelze přetěžovat operátory: .
(tečka), .* (tečka_hvězdička), :: (dvě dvojtečky), ? :
(podmíněný výraz), sizeof, typeid, dynamic_cast, static_cast,
reinterpret_cast
a const_cast. Z hlediska přetěžování lze rozdělit operátory na tři
skupiny. První skupinu tvoří operátory, které můžeme přetěžovat pouze jako
nestatické metody objektových typů. Patří sem operátory () (volání
funkce), [] (indexování), ->
(nepřímého přístupu), =
(prosté přiřazení) a operátor přetypování
(typ). Druhou (nejrozsáhlejší)
skupinu tvoří operátory, které lze přetěžovat jako nestatické metody objektových
typů nebo jako řadové funkce. Jsou to všechny operátory, které nejsou v
ostatních skupinách a nejsou operátorem, který nelze přetěžovat. Tyto operátory
musí mít alespoň jeden operand objektového nebo výčtového typu. Poslední
skupinu tvoří operátory pro správu paměti new a delete. Platí
pro ně zvláštní pravidla. Nelze změnit chování operátorů pro argumenty
standardních datových typů. Pro přetěžování operátorů používáme také termín
homonyma operátorů. Nejprve se seznámíme s obecnými pravidly přetěžování:
-
Homonyma operátorů mají stejnou prioritu a asociativu jako odpovídající
standardní operátory.
-
Homonyma operátorů zachovávají počet operandů jaký je u standardních operátorů.
-
Pro přetížené operátory nesmíme definovat implicitní hodnoty parametrů.
-
Nelze zavádět nové operátory (např. **).
-
Pokud přetížíme operátor jako nestatickou metodu, bude mít o jeden parametr
méně, prvním parametrem bude *this.
-
Pokud nám nevyhovuje definice, která má jako první parametr objekt dané
třídy, nemůžeme definovat operátor jako nestatickou metodu (v tomto případě
ji zpravidla definujeme jako spřátelenou funkci).
Mimo těchto zásad, které jsou dány definicí jazyka, je vhodné při definování
homonym operátorů dodržovat následující omezení:
-
Nesnažte se definovat homonymní operátory za každou cenu. Někdy je daleko
výhodnější použití klasických funkcí (např. výpočet datumu uprostřed mezi
dvěma daty provedeme snadněji pomocí funkce než homonymem operátoru dělení).
-
Pečlivě volte, který operátor přetížíte.
-
Rozšíření definice jednoho operátoru neznamená automatické rozšíření definic
operátorů, které s ním nějak souvisejí (např. rozšíření + neovlivňuje +=
nebo ++).
Nejprve se budeme zabývat přiřazovacími operátory. Operátor prostého přiřazení
(=) definuje překladač implicitně, kdykoli je třeba. Prostý přiřazovací
operátor můžeme přetěžovat pouze jako nestatickou metodu objektového typu,
zatímco složené operátory (např. += apod.) můžeme přetěžovat i jako funkce.
Přiřazovací operátor ve třídě X je metoda s prototypem
X& X::operator=(X&)
případně X&
X::operator=(const X&)
Je-li a instance třídy X, znamená zápis
a = b; totéž jako
a.operator=(b);
Implicitní verze přiřazovacího operátoru řeší přiřazování hodnot objektů
prostým okopírováním jednoho objektu do druhého. To nám přestane vyhovovat
např. ve chvíli, kdy naše objekty budou rozděleny na několik částí svázaných
navzájem ukazateli. V následujícím příkladu je ukázka třídy umožňující
pracovat s řetězci.
class retezec {
int delka;
char *text;
void Platny(const
char* =(const char*)2, int = 0) const;
retezec&
Prirad(const char*, int);
retezec&
Pridej(const char*, int);
friend ostream&
operator<< (ostream&, const retezec&);
public:
retezec()
{text = NULL;};
//NULL indikuje ještě nepřiřazenou hodnotu řetězci
retezec(const
char *);
retezec(const
retezec&);
retezec(int,
const char * s) {
delka = strlen(s);
text = (char*)s;
};
~retezec(){if
(text) delete text;};
retezec&
operator= (const char* s) {return Prirad(s, strlen(s));};
retezec&
operator= (const retezec& S) {return Prirad(S.text, S.delka);};
retezec&
operator+= (const char* s) {return Pridej(s, strlen(s));};
retezec&
operator+= (const retezec& S) {return Pridej(S.text, S.delka);};
};
retezec::retezec(const
char *s) {
Platny(s,
1);
delka = strlen(s);
text = new
char[delka+1];
strcpy(text,
s);
}
retezec::retezec(const
retezec& S) {
S.Platny();
delka = S.delka;
text = new
char[delka+1];
strcpy(text,
S.text);
}
void retezec::Platny(const
char* s, int i) const { //pomocná metoda kontrolující
korektnost operací
if (s &&
(i || text)) return;
cerr <<
"\n\nPoužití řetězce bez hodnoty\n\n";
abort();
}
retezec& retezec::Prirad(const
char* s, int i) {
Platny(s,
1);
if (text)
delete text;
delka = i;
text = new
char[delka+1];
strcpy(text,
s);
return *this;
}
retezec& retezec::Pridej(const
char* s, int i) {
Platny(s);
if (*s){
char* T = new char[delka+ i+1];
strcpy(T, text);
strcpy(T+delka, s);
delka += i;
delete text;
text = T;
}
return *this;
}
inline ostream&
operator<< (ostream& o, const retezec& s) {
o <<
(void*)s.text << ": " << s.delka << ">>" << s.text
<< "<<\n";
return o;
}
int main(int argc,
char **argv)
{
retezec a
= "Karel";
retezec b("Milada");
retezec bb(b);
retezec c;
const retezec
d(1, "David");
cout <<
"a="<<a<<"b="<<b<<"c="<<c<<"d="<<d;
c = a;
cout <<
"\nc2="<<c;
cout <<
"bb2="<< (bb = "Bohuslav ");
bb += d;
cout <<
"bb3=" << bb;
cout <<
"bb4=" << (bb += " Božena");
a = bb = NULL;
// nedovolená operace, bude vypsána signalizace chyby
return 0;
}
Ukazatelem s hodnotou NULL v této ukázce označujeme neinicializovanou
instanci. Je výhodné, pokud můžeme definovat neinicializující konstruktor
tak, aby operátory, které chtějí danou proměnnou použít, uměly poznat,
že dotyčná proměnná nemá přiřazenou hodnotu, a na tuto skutečnost nás nějakým
způsobem upozornili. Dále si povšimněte dvouparametrického konstruktoru,
u jehož prvního parametru není uveden žádný identifikátor. Tento parametr
nám slouží pouze k tomu, abychom odlišili daný konstruktor od druhého konstruktoru,
jehož parametrem je také textový řetězec. Tím, že jsme neuvedli u parametru
jméno, jsme překladači naznačili, že dotyčný parametr nehodláme používat
a že nás na jeho nepoužití překladač nemá upozorňovat. U tohoto konstruktoru
nealokujeme pro inicializační text místo v hromadě, ale nasměrujeme ukazatel
přímo na inicializační text (v tomto případě text nesmíme změnit).
V ukázce jsou vytvořeny i operátory přiřazení. Jsou ve dvou verzích
a to jak pro klasické textové řetězce, tak i pro řetězce právě deklarovaného
typu. Těla funkcí u těchto dvou verzí si jsou velmi podobná, a proto jsme
vytvořili pomocné funkce realizující společnou část algoritmu. Prostudujte
si implementaci uvedené třídy a vyzkoušejte.
-
Podobně jako u operátoru přiřazení můžeme přetěžovat i základní binární
operátory (+ = * / % > < >= <= == != && || | & ^ <<
>>). Identifikátor operátoru je zde tvořen klíčovým slovem operátor,
za nímž následuje symbol daného operátoru, který může být od slova operator
oddělen libovolným počtem mezer. Pokud binární operátor definujeme jako
normální funkci musí mít dva parametry a alespoň jeden z nich musí být
objektového nebo výčtového typu. U operátoru definovaného jako metoda je
jeho levým argumentem instance, jejíž metodou operátor je, takže v definici
již deklarujeme pouze jeden parametr (pravý operand). Je vhodné, když binární
operátory nemění hodnoty svých operandů a výsledek je předáván hodnotou
(použití odkazu z mnoha důvodů není vhodné). Použití binárního operátoru
+ si ukážeme na rozšíření předchozí třídy (retezec). Do třídy přidáme:
friend retezec operator+
(const retezec&, const retezec&);
a tuto spřátelenou funkci implementujeme takto:
inline retezec operator+
(const retezec& a, const retezec& b){
retezec pom
= a;
return pom
+= b;
}
Do hlavního programu vložíme na vyzkoušení např. tyto příkazy:
retezec vyrok;
vyrok = a + " a "
+ b;
cout << ("Hříšníci
" + vyrok + "!!!!");
Mohli bychom deklarovat i operátory se smíšenými parametry:
friend retezec operator+
(const char *, const retezec&);
friend retezec operator+
(const retezec&, const char *);
ale není to nutné. Ve třídě jsme definovali konverzní konstruktor s
parametrem typu char *. Překladač díky tomu umí zkonstruovat pomocný
objekt, kterému přiřadí hodnotu předávaného řetězce a který pak předá operátoru
jako skutečný parametr. Dodefinováním těchto dvou homonym operátoru sčítání
nepřidáme programu žádné nové, dříve neexistující funkce. Vyzkoušejte si
použití výše uvedeného operátoru sčítání.