6. Základy OOP VI
  1. Následující program je první z řady programů, na kterých se budeme seznamovat s virtuálními funkcemi a polymorfními objekty. Objekty jsou polymorfní, jestliže si jsou podobné, ale přesto se liší. Náš program zatím neobsahuje virtuální funkce (použijeme je až v dalších verzích). Jedná se o zjednodušenou verzi jednoho z předchozích programů. Do třídy předka byla přidána metoda nazvaná zprava. Nyní se budeme zabývat studiem operací s metodami zprava a to ve třídách předků a třídách potomků. Přidáme tedy tuto metodu i do třídy automobil a do nově přidané třídy lod. Do třídy nakladni jsme metodu zprava nepřidali, ale je zde zděděna od třídy vozidlo.

  2. V programu je deklarováno několik objektů a je jim všem zaslána zprava. Prostudujte si tento program a zdůvodněte, proč je kdy která metoda zprava volána.
    class vozidlo {
      int kola;
      float vaha;
    public:
      void zprava(void) { cout << "Vozidlo\n";}
    };
    class automobil : public vozidlo {
      int osob;
    public:
      void zprava(void) { cout << "Automobil\n";}
    };
    class nakladni : public vozidlo {
      int osob;
      float naklad;
    public:
      int pasazeru(void){return osob;}
    };
    class lod : public vozidlo {
      int osob;
    public:
      int pasazeru(void){return osob;}
      void zprava(void) { cout << "Loď\n";}
    };
    int main(int argc, char **argv)
    {
      vozidlo unicykl;
      automobil sedan;
      nakladni tatra;
      lod plachetnice;
      unicykl.zprava();
      sedan.zprava();
      tatra.zprava();
      plachetnice.zprava();
      unicykl = sedan;
      return 0;
    }
  3. Předchozí verzi programu nyní nepatrně změníme. Na začátek červeného řádku, před deklaraci metody zprava ve třídě předka přidáme klíčové slovo virtual. Po spuštění programu zjistíme, že funkce programu se nezměnila. Je to z toho důvodu, že objekty používáme přímo a virtuální metody nemají nic co dělat s objekty, ale pouze s ukazateli na objekty, jak bude ukázáno dále. Modrý příkaz v programu ukazuje, že přestože všechny čtyři objekty v programu jsou různých tříd, je možno přiřadit potomka předku.
  4. Vrátíme se opět k původní verzi našeho programu (odstraníme tedy přidané klíčové slovo virtual) a program změním tak, aby používal ukazatele na objekty. Následuje pouze výpis těla funkce main (jinak se program nezměnil; vynechali jsme také poslední příkaz).

  5. vozidlo *unicykl;
    unicykl = new vozidlo;
    unicykl->zprava();
    automobil *sedan;
    sedan = new automobil;
    sedan->zprava();
    nakladni *tatra;
    tatra = new nakladni;
    tatra->zprava();
    lod *plachetnice;
    plachetnice = new lod;
    plachetnice->zprava();
    Po spuštění tohoto programu zjistíte, že pracuje opět stejně jako původní verze. V programu jsme neprovedli dealokaci objektů před ukončením programu. Doplňte potřebné příkazy sami.
  6. Do našeho upraveného programu opět vraťte klíčové slovo virtual a program vyzkoušejte. Zjistíte, že program pracuje opět stejně. Je to z důvodu, že při použití ukazatelů na každý objekt je ukazatel stejného typu jako objekt na který ukazuje.
  7. Předchozí verze našeho programu ukazují jak virtuální funkce nepracují, nyní si již ukážeme jak virtuální funkce pracují. Program změníme tak, že ve funkci main již nebudeme používat čtyři různé ukazatele na různé objekty, ale pouze jeden a to na třídu předka. Hlavní program bude tedy vypadat takto:

  8. vozidlo *unicykl;
    unicykl = new vozidlo;
    unicykl->zprava();
    delete unicykl;
    unicykl = new automobil;
    unicykl->zprava();
    delete unicykl;
    unicykl = new nakladni;
    unicykl->zprava();
    delete unicykl;
    unicykl = new lod;
    unicykl->zprava();
    delete unicykl;
    Nejprve vyzkoušíme verzi s vynechaným klíčovým slovem virtual, tedy případ, kdy se nejedná o virtuální funkce. Jelikož používáme stále typ ukazatele na třídu vozidlo, bude vždy při zaslání zprávy zprava kterémukoli našemu objektu vyvolána metoda zprava třídy vozidlo.
    V našem programu vidíme, že můžeme použít ukazatel na jednu třídu k odkazování se na jinou třídu. Jestliže se odkazujeme na vozidlo (v reálném světě a ne jen v našem programu), můžeme se odkazovat na automobil, nákladní auto, motocykl nebo jiný typ dopravního prostředku, neboť je to obecnější forma objektu. Nicméně, pokud se odkazujeme na automobil nelze již použít nákladní auto, motocykly a jiné typy dopravních prostředků, protože automobil je již specializovanou třídou. Obecněji termínem vozidlo se můžeme odkazovat na mnoho typů vozidel, ale specifičtějším termínem automobil pouze na jeden typ vozidel nazvaný automobil. Ukazatel na třídu předka může být použit k ukazování na objekt odvozené třídy od této třídy předka, ale ukazatel na odvozenou třídu nelze použít k ukazování na třídu předka nebo jinou odvozenou třídu od třídy předka.
    Do programu nyní opět přidáme klíčové slovo virtual. Tím změníme metodu na virtuální a zajistíme dynamické sestavování neboli polymorfismus. Nyní se určuje volaná metoda zprava (je to virtuální metoda) na základě skutečného objektu na který ukazatel ukazuje a to až při provádění výpočtu (není-li použito virtual je volaná metoda určena již při překladu programu). Toto dynamické sestavování je v některých situacích velmi užitečné. Virtuální funkce musí být implementována v rodičovské třídě a v odvozených třídách musí být stejného typu, mít stejný počet a typy parametrů. Ve všech odvozených třídách musí být její hlavička identická. Klíčové slovo virtual nemusí být již u virtuálních funkcí v odvozených třídách použito.
    V  našem programu přidejte i do třídy nakladni metodu zprava a ověřte, že tato metoda bude používána v rámci této třídy namísto metody zděděné od třídy předka.
  9. Vrátíme se opět k souborovým datovým proudům. Soubory mohou být otevírány v různých režimech. Např. někdy můžeme chtít přidat data na konec existujícího proudu namísto vytváření nového souboru. V tomto případě soubor otevíráme v režimu app. To zadáváme v konstruktoru ofstream:

  10. ofstream vystsoubor("Test.dat", ios::app);
    Tento soubor byl otevřen tak, že všechna data budou zapisována na konec souboru. Povšimněte si, že příznak app používá operátor rozsahu pro třídu ios. To proto, že tyto příznaky jsou definovány ve třídě ios (prapředek všech tříd datových proudů). Specifikátory režimů souborových datových proudů jsou uvedeny v následující tabulce:
     
    Specifikátor Popis
    app Otevírá soubor a všechna nová data umisťuje na jeho konec.
    ate Ukazatel souboru je při otevření umístěn na konec souboru.
    in Otevírá soubor pro vstup (čtení). Je to implicitní příznak pro třídu ifstream.
    out Otevírá soubor pro výstup (zápis). Je to implicitní příznak pro třídu ofstream.
    binary Otevírá soubor v binárním režimu (implicitně je soubor otevírán v textovém režimu).
    trunc Otevírá soubor a zruší jeho obsah (pokud není specifikováno app nebo ate, pak trunc je implicitní).
    Je možno použít i více specifikátorů najednou. Např. když otvíráme soubor v binárním režimu pro přidávání nových dat na konec souboru, pak použijeme:
    ofstream vystsoubor("test.dat", ios::app | ios::binary);
  11. S binárními daty se pracuje jiným způsobem než s textovými daty. Data musí být zapsána v nějakém logickém uspořádání a čtena stejným způsobem. To nám usnadňují struktury. Např. struktura:

  12. struct osoba {
      char Jmeno[20];
      char Telefon[20];
      int Vek;
      int IdentCis;
    };
    poskytuje logické uspořádání dat. Pro zápis obsahu této struktury do souboru použijeme třídu ofstream takto:
    osoba MojeData = {"Karel Novák", "není", 41, 1};
    ofstream vystsoubor("jmena.dat", ios::binary);
    vystsoubor.write((char *)&MojeData, sizeof(osoba));
    Metoda write třídy ofstream akceptuje jako první parametr char * a musíme tedy adresu struktury přetypovat. Počet zapsaných slabik je určen druhým parametrem. Čtení binárních dat je také snadné:
    ifstream vstsoubor("jmena.dat", ios::binary);
    if (!vstsoubor) return 0;
    osoba MojeData;
    vstsoubor.read((char *)&MojeData, sizeof(osoba));
  13. Jednou z důležitých věcí při používání tříd souborových proudů je pozice souboru. Pozice souboru je číselná hodnota, která určuje následující čtenou slabiku (v případě ifstream) nebo zapisovanou slabiku (v případě ofstream). Po otevření souboru je pozice souboru nastavena na 0. Pokud přečteme 10 slabik dat, pak se pozice souboru změní na 10. Po přečtení dalších 20 slabik dat má pozice souboru hodnotu 30. Pozice souboru je automaticky aktualizována při čtení a zápisu dat.

  14. I když pozice souboru je automaticky aktualizována, jsou metody, které můžeme použít k zjištění nebo nastavení pozice souboru. Pro třídu ifstream metoda seekg nastavuje pozici souboru na specifikovanou hodnotu a metoda tellg vrací současnou pozici souboru. Pomocí těchto dvou metod můžeme určovat, která data budou čtena ze souboru. Pro třídu ofstream tyto úlohy provádějí metody seekp a tellp.
    Jednotlivé slabiky z binárního souboru lze číst metodou get a zapisovat je metodou put. Např. následující úsek programu vytváří kopii souboru:
    ifstream vstsoubor("jmena.dat", ios::binary);
    ofstream vystsoubor("docasny.soub", ios::binary);
    vstsoubor.seekg(0, ifstream::end);
    int pocetSlabik = vstsoubor.tellg();
    vstsoubor.seekg(0);
    for (int i = 0; i < pocetSlabik; i++) {
      char c;
      vstsoubor.get(c);
      vystsoubor.put(c);
    }
    Povšimněte si použití metod seekg a tellg v předchozím kódu. První metoda seekg přesouvá indikátor pozice souboru na konec souboru (0 slabik od ifstream::end - určuje konec souboru). Metoda tellg zjistí pozici souboru, tj. velikost souboru ve slabikách. Druhá metoda seekg přesune indikátor pozice na začátek souboru a v cyklu jsou postupně zpracovávány jednotlivé slabiky souboru.
  15. Předpokládejme, že máme soubor, ve kterém je uloženo 1000 záznamů a potřebujeme přečíst záznam čísla 999. Můžeme to vyřešit tak, že v cyklu budeme číst jednotlivé záznamy až do požadovaného záznamu nebo nastavit pozici souboru přímo na záznam 999 a přečíst právě jen požadovaný záznam. Náhodný přístup k záznamům souboru je efektivní v případě, kdy soubor je tvořen záznamy známé délky nebo jestliže známe přesně rozmítění záznamů v souboru. Pokud známe velikost záznamu, pak pozici získáme jednoduchým výpočtem. Např. v případě struktury osoba to bude:

  16. int pos = 998 * sizeof(osoba);
    První záznam je na pozici 0 a tedy 999 záznam je na pozici 998 * velikost struktury. Můžeme tedy otevřít soubor, přejít na pozici záznamu 999 a záznam přečíst:
    ifstream vstsoubor("jmena.dat", ios::binary);
    vstsoubor.seekg(pos);
    osoba MojeData;
    vstsoubor.read((char *)&MojeData, sizeof(osoba));
    Podobně můžeme nahradit nebo aktualizovat záznam v souboru:
    ofstream vystsoubor("jmena.dat", ios::binary | ios::ate);
    int pos = 998 * sizeof(osoba);
    vystsoubor.seekp(pos);
    vystsoubor.write((char *)&MojeData, sizeof(osoba));
    Zde musíme použít příznak ate (stejný výsledek dostaneme i při použití app), abychom zabránili přepsání souboru při jeho otevření.
  17. Pokud potřebujeme otevřít soubor jak pro čtení, tak i pro zápis, pak musíme použít třídu fstream. Tato třída je potomkem tříd ifstream a ofstream a tedy třída fstream dědí vše od obou tříd předků. Následující úsek kódu ukazuje, jak použít fstream k přehození prvního a desátého záznamu v souboru:

  18. osoba zaznam1;
    osoba zaznam2;
    fstream soubor("jmena.dat", ios::binary | ios::in | ios:: out);
    soubor.seekg(9 * sizeof(osoba));
    soubor.read((char *)&zaznam1, sizeof(osoba));
    soubor.seekg(0);
    soubor.read((char *)&zaznam2, sizeof(osoba));
    soubor.seekg(0);
    soubor.write((char *)&zaznam1, sizeof(osoba));
    soubor.seekg(9 * sizeof(osoba));
    soubor.write((char *)&zaznam2, sizeof(osoba));
    soubor.close();
    Abychom soubor mohli číst i zapisovat do něj, je nutno v konstruktoru fstream použít příznaky in i out.

Nové pojmy:


 
6. Základy OOP VI