Po dlouhé odmlce Vás opět vítám u dalšího dílu seriálu C++ krok za krokem. Hned v úvodu připojuji svou omluvu za to, že další díl vychází po tak dlouhé době – jednak jsem neměl moc času, ale abych nepoužíval jen obligátní výmluvu typu „není čas“, přiznávám, že i moje lenost má jistý podíl na téměř půlroční pauze.
Protože je to již dlouho, co jste četli předchozí díl, připomenu, co jsme se minule naučili a na co dnes navážeme. Předmětem předchozího dílu byla dědičnost – měli byste nyní tedy vědět, co to je, jak se používá a jaké jsou její druhy. Dnes na dědičnost navážeme, konkrétně se budeme „bavit“ o virtuálních metodách. Začneme však překrýváním metod.
Překrývání metod
Abychom mohli navázat na předchozí díl, je nutno mít napsaný kód, ve kterém bude zakomponována dědičnost. Pokud nemáte kód z minulého dílu, pro naše účely nám postačí následující dvě třídy.
programator.h
#include <iostream>
#include <string>
using namespace std;
class Programator : public Zamestnanec
{
private:
string progJazyk;
public:
Programator(string jm, string progJaz)
{
jmeno = jm;
progJazyk = progJaz;
}
};
zamestnanec.h
using namespace std;
class Zamestnanec
{
protected:
string jmeno;
public:
Zamestnanec()
{
jmeno = "pan neznamy";
}
Zamestnanec(string jm)
{
jmeno = jm;
}
void Pracuj()
{
cout << "Ja jsem zamestnanec " << jmeno << " a prave pracuji.\n";
}
};
Vidíme, že máme nějakou třídu Zamestnanec, která má atribut jmeno a metodu Pracuj, která vypíše, že zaměstnanec s nějakým jménem právě pracuje. Pro zopakování si tuto metodu zkusíme zavolat.
#include <stdio.h>
#include "zamestnanec.h"
#include "programator.h"
int main()
{
Zamestnanec ZamA("Pepa");
Programator ProA("Lojza", "Java");
ZamA.Pracuj();
ProA.Pracuj();
return 0;
}
Když to spustíme, vypíše se, že zaměstnance Pepa právě pracuje a také že zaměstnanec Lojza pracuje. Lojza je ale programátor, nebylo by tedy lepší, kdyby třída Programator vypisovala při zavolání metody Pracuj něco ve smyslu “Ja jsem programator <jmeno> a prave usilovne programuji v jazyce <progJazyk>” ? Asi by to bylo lepší, už jen proto, že bychom z výpisu hned poznali, co kdo dělá. Jak to tedy udělat? Celkem snadno, ve třídě Programator metodu Pracuj přepíšeme, nebo chcete-li překryjeme. Třídu tedy upravíme takto:
#include <iostream>
#include <string>
using namespace std;
class Programator : public Zamestnanec
{
private:
string progJazyk;
public:
Programator(string jm, string progJaz)
{
jmeno = jm;
progJazyk = progJaz;
}
void Pracuj()
{
cout << "Ja jsem programator " << jmeno << " a prave usilovne programuji v jazyce " << progJazyk << ".\n";
}
};
Pokud program spustíme znovu, uvidíme, že výstup se změnil a je z něho vidět, že Lojza je programátor a usilovně programuje v jazyku Java. Potřebujeme-li tedy aby metoda nějaké třídy, která dědí z jiné třídy, fungovala jinak než metoda nadřazené třídy, prostě ji přepíšeme. Tím zajístíme, že se bude volat tato nově přepsaná metoda a ne metoda nadřazené třídy.
Pointery jsou zpět
V některém z minulých dílů jsme se naučili, co to jsou pointery, neboli ukazetele. Jednalo se však o ukazatele na primitivní datové typy, teď budeme pracovat s pointery na naše třídy. Předpokládejme, že v našem programu chceme mít více zaměstnanců a všechny je budeme chtít uložit do jednoho pole – a bude nám jedno, zda to bude programátor, uklízečka, kuchař a kdo ví co ještě – prostě všechny do jednoho pole. Jaký datový typ pole tedy zvolit?
Řešení spočívá v použití ukazatele na typ Zamestnanec. Podívejte se na kód:
#include <stdio.h>
#include "zamestnanec.h"
#include "programator.h"
int main()
{
const int N = 3; // Pocet zamestnancu
Zamestnanec* zamestnanci[N]; // Pole zamestnancu
zamestnanci[0] = new Zamestnanec("Pepa");
zamestnanci[1] = new Programator("Cenda", "Fortran");
zamestnanci[2] = new Programator("Franta", "C#");
for (int i = 0; i < N; i++)
{
zamestnanci[i]->Pracuj();
}
return 0;
}
Možná si říkáte, že tohle přece nemůže fungovat, nebo se ptáte, co má znamenat ta šipka “->”. S tou šipkou (pomlčka a zobák doprava) je to snadné – máme-li pointer, který ukazuje na nějakou instanci třídy, metodu této instance zavoláme přes název pointeru, šipku a název metody. Stejně tak by šlo přistupovat i k veřejným atributům třídy. Zkrátka a jednouduše, pokud se jedná o ukazatel na instanci, místo tečky používáme šipku.
Druhou otázkou však je, jak je možné mít pointer typu Zamestnanec, který ale ukazuje na objekt třídy Programator? Proto je důležité vědět, že reference nebo ukazatel na základní třídu může odkazovat na instanci třídy odvozené. Jinými slovy – máme li obecně třídu A, ze které bude dědit dalších milion tříd s názvem B1, B2…BN, je možné mít ukazatel na třídu A, který bude ukazovat na některou z tříd B. Funguje to i dále, pokud ze třídy B bude dědit třída třída C, pořád je možné mít ukazatel na A, který bude ukazovat na třídu C. Opačný proces, tedy aby ukazatel typu B ukazoval na instanci třídy A se neobejde bez explicitního přetypování, o tom ale někdy jindy.
Teď, když už snad není divu tomu, že kód skutečně bude fungovat, tu naší úžasnou aplikaci spustíme. Další problém je na světě. Program sice funguje, ale ne tak, jak bychom čekali. Čenda i Franta jsou programátoři, ale místo aby vypsali, že programují v nějakém jazyce, vypsali jen to, že právě pracují – a to my přece nechceme. Abychom tomuto článku dodali trochu odbornějších termínu, můžeme říct, že se použila tzv. časná, neboli také statická vazba.
Statická a dynamická vazba
Pojďme se trochu blíže podívat na již zmíněnou statickou vazbu. V době kompilace kompilátor nemá tušení, na jaký typ objektu bude ukazal zamestnanci ukazovat. Proto ani neví, která metoda bude za běhu vlastně provedena. Proto mu nezbývá jiná možnost než porovnat metodu třídy s typem ukazatele. Typ ukazatele je v tomto případě Zamestnanec, proto se zavolá metoda třídy Zamestnanec, která pouze vypisuje, že nějaký zaměstnanec pracuje. Opakem statické vazby je vazba dynamická.
Nyní tedy víme, co je to statická vazba a také víme to, že jejím opakem je dynamická vazba. Ta nám zajistí, že se zavolá správná metoda, kterou bychom očekávali. Pořád ale nevíme jeden malý detail – jakým zázračným způsobem tu dynamickou vazbu “aktivovat”.
Kouzelné slovo virtual
Jediným klíčovým slovem, dopsaným na správné místo v kódu, zajistíme to, že se metoda Pracuj stane virtuální, čímž se aktivuje dynamická vazba. V souboru zamestnanec.h se podívejte na řádek, kde začíná metoda Pracuj. Před klíčové slovo void dopište slovo virtual a spusťte program – problém vyřešen.
virtual void Pracuj()
{
cout << "Ja jsem zamestnanec " << jmeno << " a prave pracuji.\n";
}
Touto malou změnou jsme zajistili aktivaci dynamické vazby a na výstupu programu se nám již objeví, že programátoři usilovně programují v nějakém programovacím jazyku.
Slovo virtual stačí napsat k metodě v základní třídě, u zdědených tříd to již potřeba není, nicméně pokud slovo dopíšete i tam, nebude to na škodu. Naopak někdy to bývá dobrým zvykem, neboť je pak hned jasné, že daná metoda je virtuální.
Jak to funguje “uvnitř”
Už je nám tedy jasné, co nám zajišťují virtuální funkce a dynamická vazba, někoho by však mohlo zajímat, jak to vlastně všechno funguje tam někdě “uvnitř”. Pro úplnost budeme tedy ještě krátký odstavec věnovat základnímu náhledu, jak fungují virtuální metody.
Kompilátor s virtuálními metodami pracuje tak, že do každého objektu přidá jistou skrytou položku, která obsahuje ukazatel na pole adres jednotlivých metod. Toto pole se pro jednoduchost nazývá tabulka. Tato tabulka obsahuje adresy všech virtuálních metod, které jsou pro objekty dané třídy deklarovány. Objekt nějaké odvozené třídy pak bude obsahovat samostatnou tabulku adres. Pokud odvozená třída nově zadefinuje nějakou virtuální metodu, bude tabulka obsahovat adresu této virtuální metody. Zkrátka lze zjedodušeně říci, že přidáním nové virtuální metody se do tabulky přidá jedna položka s adresou.
V případě, že zavoláme v programu virtuální metodu, program se podívá do tabulky a na základě této tabulky se pak zavolá příslušná metoda.
Lze tedy řici, že používání virtuálních funkcí je o něco málo náročnější na paměť a také na rychlost provedení. Musí se totiž provést jeden krok navíc, kterým je vyhledání adresy v tabulce.
Shrnutí
V tomto díle jsem si ukázali, jak lze překrýt metodu odvozené třídy, vysvětlili jsme si pojmy statická a dynamická vazba a nakonec jsme se naučili, co dělá klíčové slovo virtual. Mohli byste si zkusit dopsat ještě nějakou třídu (např. Uklizecka, Kuchar nebo něco podobného), kterou podědíte z třídy Zamestnanec a zkuste si sami naimplementovat nějakou jinou virtuální metodu. Zkrátka by bylo fajn si s tím trochu “pohrát”, ať si to jak se říká “osaháte”. V příštím díle (na který se nebude čekat půl roku) se podíváme na abstraktní třídy, případně si povíme i něco o statických atributech a statickým metodách.
V případě, že Vám nebude něco jasné, nebo pokud jsem zapomněl něco zmínit (což se mohlo lehce stát), zeptejte se v diskusi.