Virtuální metody

13. díl - Virtuální metody

Petr Sklenička       1. 2. 2012       C++/C       10004 zobrazení

V dnešním dílu jsou na řadě tzv. virtuální metody. Naučíme se tedy, co to virtuální metody jsou a jakým způsobem se používají. Mimo to se ale také naučíme překrývat nějakou metodu a bude zmínka i o statické a dynamické vazbě.

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.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Všechny díly tohoto seriálu

 

Mohlo by vás také zajímat

C++/CLI a interoperabilita managed a unmanaged kódu - díl 1.: Úvod do jazyka a základní konstrukce

V tomto článku je popsána nadstavba C++ pro práci s .NET prostředím zvaná C++/CLI umožňující vytvářed mixed assembly obsahující jak managed tak unmanaged kód. V prvním díle je popsána myšlenka jazyka a základní syntaktické konstrukty (základní typy, podmínky, cykly, pole, namespace a část tříd a objektů). U čtenáře je předpokládána znalost .NET frameworku a nativního programování nejlépe v C++ (alespoň syntaxi a základy).

C++/CLI a interoperabilita managed a unmanaged kódu - díl 2.: Složitější konstrukty, low-level přístup a generické programování

V tomto článku je popsána nadstavba C++ pro práci s .NET prostředím zvaná C++/CLI umožňující vytvářed mixed assembly obsahující jak managed tak unmanaged kód. V tomto díle jsou popsány pokročilejší partie jazyka týkající se hlavně objektů, low-level přístupu k managed objektům a generického programování.

Virtuální souborový systém

Článek pojednává o způsobu návrhu virtuálního souborového systému. Před čtením Vám doporučuji stáhnout si a přečíst zadání, abyste věděli o co vlastně jde. Ke stažení jsou i zdrojové kódy v jazyce C++. Aplikace asi nemá žádné velké využití, nicméně přišlo mi to jako zajímavý problém - jednalo se o semestrální projekt.

 

 

Nový příspěvek

 

článek

Díky za supr článek :) je to přehledný a jednoduše vysvětlené...myslím že Céčko se opravdu má cenu učit, já sem se dřív učil jenom HTML + CSS, ale to si myslím že je takovej základ, protože je to opravdu jednoduchý...jestli se chce někdo živit programováním, tak bych řekl že Céčko je k tomu ideální. A kdyby chtěl někdo nahnat trošku praxe, doporučil bych mu web http://itprojekty-nemecko.cz/,... mě se to docela vyplatilo a celkem dost sem se v práci naučil :)

PS

Neuvažuješ o nějakém pokračování? Bylo by to skvělý

nahlásit spamnahlásit spam 0 odpovědětodpovědět

//

Velice pěkné články o C++. Nechtěl by v nich někdo pokračovat? Petr to jak tak koukám dávno odpískal. (Což je velká škoda)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Virtuální metody

Pěkný článek, jen bych chtěl upřesnit pár faktických drobností:

1) odkaz na VMT (virtual method table) je jen u polymorfických typů, tj takových, které mají aspoň jednu virt. metodu. Jaký je v tom rozdíl? Např. v tom, že na nepolymorfické typy nefunguje dynamic_cast (nemá podle čeho zjišťovat typ) a také to třeba poznáte, pokud "serializujete" objekty prostým zapsáním binární reprezentace v paměti (prostě fwrite) - nepolymorfické jednoduché takhle jdou, polymorfické libovolné ne (je tam problém s tím pointerem na VMT).

2. Při volání virt. metod se nic nevyhledává, jen se skočí přímo na adresu (kompilátor ví, že adresa metody XY je na offsetu 0x12 od začátku VMT)

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Je to pěkně vysvětlené, já jsem spokojen, jen tak dál :-)

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback