Vítejte u prozatím poslední části kurzu C++. V tomto článku se budeme věnovat identifikaci typu instance objektu za běhu programu, pak přejdeme k vstupním a výstupním operacím s použitím objektových vstupně-výstupních proudů, které patří do knihovny jazyka C++.
V minulé lekci jsem v příkladu k operátoru dynamic_cast zapomněl uvolnit paměť, takže pro úplnost je třeba doplnit následující:
char c;
cin >> c;
delete
r1;
delete r2;
return 0;
Někdy se nám může hodit znalost typu instance, se kterou právě pracujeme. Obzvlášť pokud máme abstraktní bázovou třídu a od ní odvozené třídy, tyto třídy jsou tedy polymorfní. K tomuto účelu slouží operátor typeid, definovaný v hlavičkovém souboru typeinfo, který musíme tedy do našeho programu přidat. Hlavičkový soubor existuje i ve verzi s příponou - typeinfo.h. Rozhodl jsem se ale použít to, co jsme se minule naučili o prostorech jmen, program bude vypadat víc "C++-ově". Budeme tedy muset použít direktivu using, protože operátor leží v prostoru jmen std.
Operátor se používá jedním z následujících způsobů:
typeid(jmeno)
typeid(vyraz)
První zápis vrátí hodnotu, kterou pak můžeme použít pro porovnání. Druhý zápis pak zjistí typ výrazu, výraz se přitom nevyhodnocuje. Výsledkem operace je konstantní objekt typu type_info, který je taktéž definován v souboru typeinfo (resp. typeinfo.h).
Nyní si ukážeme příklad:
#include <iostream>
#include <typeinfo>
using namespace std;
class Rodic {
Tisk() { cout << "Rodic" << endl; }
};
class Potomek : public Rodic {
Tisk() { cout << "Potomek" << endl; }
};
int main(int argc, char* argv[])
{
Rodic r;
Potomek p;
Rodic *ptr_r;
ptr_r = &r;
cout << "Promenna r ma typ : " << typeid(r).name() << endl;
cout << "Promenna p ma typ : " << typeid(p).name() << endl;
cout << "Promenna ptr_r ma typ " << typeid(ptr_r).name() << " a ukazuje na : "
<< typeid(*ptr_r).name() << endl;
ptr_r = &p;
cout << "Promenna ptr_r ma typ " << typeid(ptr_r).name() << " a ukazuje na : "
<< typeid(*ptr_r).name() << endl;
char c;
cin >> c;
return 0;
}
Výstup v následující podobě nás asi moc nepřekvapí:
Promenna r ma typ : class Rodic
Promenna p ma typ : class Potomek
Promenna ptr_r ma typ class Rodic * a ukazuje na : class Rodic
Promenna ptr_r ma typ class Rodic * a ukazuje na : class Rodic
Ale pokud z třídy Rodic uděláme abstraktní třídu, tedy bude obsahovat alespoň jednu metodu s klíčovým slovem virtual:
class Rodic {
virtual Tisk() { cout << "Rodic" << endl; }
};
Bude výstup vypadat takto:
Promenna r ma typ : class Rodic
Promenna p ma typ : class Potomek
Promenna ptr_r ma typ class Rodic * a ukazuje na : class Rodic
Promenna ptr_r ma typ class Rodic * a ukazuje na : class Potomek
Pro úspěšné zkompilování příkladu s polymorfními typy je nutné v některých překladačích zapnout podporu pro dynamickou identifikaci typů (RTTI). Pro vývojové prostředí Microsoft Visual C++ 6.0 je postup zapnutí RTTI následující: otevřete menu Project, zvolte Settings. V okně Settings pak vyberte kartu C/C++ a v listboxu Category vyberte C++ Language. Zaškrtněte volbu Enable RTTI. Pozor na to, že volbu je nutné zapnout zvlášť pro Debug i Release verzi.
Z příkladu vidíme, že pokud použijeme nepolymorfní třídu (bez virtuálních metod), vypíše se deklarované jméno třídy, na kterou ukazuje tento ukazatel. K vyhodnocení totiž dojde již v době překladu. Pokud bude třída polymorfní, pak se vyhodnocení provede až za běhu programu. Jestliže jako parametr pro operátor typeid chceme použít ukazatel, pak ho musíme dereferencovat pomocí *, jinak bychom dostali informaci o typu ukazatele, tedy např. class Rodic *, jak vidíme ve výstupu.
Kód naleznete v sekci Downloads (projekt Priklad1).
Pokud bychom použili typeid na ukazatel s hodnotou NULL, dojde k vyvolání výjimky typu bad_typeid. Ukážeme si příklad:
#include <iostream>
#include <typeinfo>
using namespace std;
class A {
virtual f() { ; }
};
int main(int argc, char* argv[])
{
A *ptr_a = new A;
cout << "Ukazatel ukazuje na typ: " << typeid(*ptr_a).name() << endl;
try {
delete(ptr_a);
ptr_a = NULL;
cout << "Ukazatel ukazuje na typ: " << typeid(*ptr_a).name() << endl;
}
catch(bad_typeid)
{
cout << "Identifikace ukazatele s hodnotou NULL" << endl;
}
catch(...)
{
cout << "Divne chovani!!!" << endl;
}
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad2).
K tomuto nevinně vypadajícímu prográmku je nutné uvést dvě poznámky.
Podle toho, co jsme si řekli bychom po spuštění měli dostat následující výpis:
Ukazatel ukazuje ma
typ: class A
Identifikace ukazatele s hodnotou NULL
Pokud však program přeložíme překladačem Microsoft Visual C++ 6.0 (bez Service pack) dostaneme následující:
Ukazatel ukazuje ma typ: class A
Divne chovani!!!
Vidíme tedy, že výjimka byla zachycena handlerem ..., který je určen pro ostatní výjimky. Po překladu pomocí Microsoft Visual Studio .NET program funguje tak, jak bychom očekávali. Poslední Service Pack 5 pro Microsoft Visual C++ 6.0 jsme bohužel neměli k dispozici. Pokud však použijeme hlavičkové soubory s příponami .h a vypustíme direktivu using, pak vše funguje i pod Microsoft Visual C++ 6.0. Zde je úprava:
#include <iostream.h>
#include <typeinfo.h>
class A {
virtual f() { ; }
};
// a dale stejne
Kód naleznete v sekci Downloads (projekt Priklad2opr).
Pokud ve třídě A nebude virtuální funkce a půjde tedy o nepolymorfní třídu, nedojde k vyvolání výjmky a operátor typeid nám vrátí typ na který ukazuje ptr_a, tedy class A. Typ byl vyhodnocen již v době překladu.
Kromě výjimky bad_typeid může být vyvolána ještě výjimka typu __non_rtti_object. Tato výjimka značí chybu přístupu (může to být například při kompilaci kódu bez zapnuté RTTI nebo pokud je ukazatel neplatný). Třída __non_rtti_object je zděděna od třídy bad_typeid a proto by v seznamu handlerů výjimek měla předcházet třídě bad_typeid, jinak dojde k jejímu zachycení handlerem pro bad_typeid.
Nyní se podíváme na třídu type_info. Prozatím jsme z této třídy použili její metodu name(), která vrací jméno identifikovaného typu (formát řetězce závisí na konkrétní implementaci). Třída obsahuje ještě metodu raw_name(), která vrací neupravené jméno, např. .?AVA@@. Jak vidíte, toto jméno je pro člověka nečitelné, ale pokud ho použijeme při porovnání dvou tříd, pak dosáhneme zrychlení programu, protože se program nemusí zdržovat s převodem jména na čitelnou verzi. Dalšími metodami jsou operátory porovnání == a !=. Za zmínku ještě stojí metoda before(), která lexikograficky porovnává řetězce identifikující typ. Důležitou informací je, že instance této třídy nelze vytvářet přímo, mají totiž soukromý operátor kopírování a přiřazení. Dočasnou instanci tohoto typu lze tedy dostat jen pomocí operátoru typeid.
V článcích o programování v C++ jsme doposud používali vstupně-výstupních proudů jen jako náhrady za funkce scanf() a printf(). Nyní si ukážeme jak proudy použít i pro vstup nebo výstup ze souboru, jak upravit třídu, abychom ji mohli jednoduše vypsat pomocí výstupních proudů. Také uvidíme, jak formátovat výstup a další.
Pokud nám práce s objektovými proudy nevyhovuje, pak nic nebrání v používání starých vstupně-výstupních operací pomocí knihovny jazyka C.
Doposud jsme se seznámili s proudy cin a cout, standardně však v knihovně jazyka C++ existují ještě proudy cerr a clog. Proud cin je instancí třídy istream, která je určena pro vstup ze standardního vstupního proudu (v jazyce C to byl stdin), tedy nejčastěji vstup z klávesnice (lze ovšem přesměrovat). Instance cout třídy ostream slouží k výstupu do standardního výstupního proudu (v jazyce C stdout), nejčastěji tedy konzole (ale opět lze přesměrovat). Proudy clog a cerr jsou také instancemi třídy ostream, ale jejich výstup směřuje do standardního chybového proudu (stderr), rozdíl mezi nimi je pak v užití vyrovnávací paměti. Zatímco cerr zapisuje rovnou, pro výstup do clog se využívá vyrovnávací paměť. Tyto třídy jsou instancializovány v hlavičkovém souboru iostream, resp iostream.h. Třídy istream a ostream jsou virtuálně zděděny od společného předka - třídy ios_base, ve starších verzích knihovny jazyka C++ to je jen ios. Virtuální dědění je využito, aby ve výsledné třídě nebyly dvakrát obsaženy stavové proměnné proudu (viz. lekce 15). Navíc existuje objekt iostream, který lze použít pro vstupní i výstupní operace. Rodičovskými třídami tohoto objektu jsou třídy istream a ostream.
Další odvozené třídy nám umožňují práci se soubory a jsou to následující: ifstream (vstupní), ofstream (výstupní) a fstream (vstupně-výstupní). Tyto třídy jsou implementovány v souboru fstream, resp. fstream.h. Proudy mohou také pracovat s tzv. paměťovými proudy, jejich jména jsou istringstream (vstupní), ostringstream (výstupní) a stringstream (vstupně-výstupní). Implementace je v souboru sstream. Tyto proudy ke své funkci využívají další knihovní třídu - třídu string - která zajišťuje práci s řetězci. Z důvodu kompatibility se staršími verzemi jsou pak ještě k dispozici třídy istrstream, ostrstream a strstream, které nevyužívají výše zmíněnou třídu string, ale pracují s poli znaků.
Jak už jsme viděli, ke vstupu používáme operátor >>, resp. operátor << k výstupu. Tyto přetížené operátory jsou, pro standardní datové typy, umístěny ve třídách istream, resp. ostream. Protože od těchto tříd jsou zděděny ostatní třídy, máme tyto operátory k dispozici i u nich.
Pro odřádkování jsme používali manipulátoru endl. Tyto manipulátory upravují stav proudu a mohou také např. měnit formátování, jak uvidíme dále.
Nejprve si ukážeme, jak použít manipulátory pro formátování výstupu, podobně jako u funkce printf(). Ukážeme si nejdříve příklad:
#include <iostream>
#include <iomanip>
using namespace std;
int main(int argc, char* argv[])
{
float f = 1.0825f;
int a = 244;
cout << "Vypis integeru (5): " << setfill('0') << setw(5) << a << endl;
cout << "Presnost nastavena na 3: " << setprecision(3) << f << endl;
cout << "Vypis integeru : " << a << endl;
cout << "Hexadecimalne: " << setbase(16) << a << endl;
cout << "Hexadecimalne: " << a << endl;
cout << "Osmickova soustava: " << setbase(8) << a << endl;
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad3).
Následuje výstup programu:
Vypis integeru (5):
00244
Presnost nastavena na 3: 1.08
Vypis integeru : 244
Hexadecimalne: f4
Hexadecimalne: f4
Osmickova soustava: 364
Pro použití těchto manipulátorů musíme vložit soubor iomanip (nelze iomanip.h, alespoň v MSVC 6), kde jsou definovány manipulátory s parametry. Vidíme, že některé manipulátory ovlivní výstupní proud trvale (dokud je znova nezměníme), jiné platí jen pro následující vypisovanou proměnnou. Mezi trvale ovlivňující patří např. změna báze číselné soustavy, přesnost čísla v plovoucí čárce. Mezi dočasně ovlivňující pak nastavení šířky následovně vypsaného čísla a s ním spojený vyplňující znak.
Dalšími manipulátory jsou pak např. fixed, scientific nebo showpoint. Ukážeme si opět příklad:
#include <iostream>
#include <iomanip>
using namespace std;
int main(int argc, char* argv[])
{
float f = 2.000000f;
cout << "Bez nastaveni: " << f << endl;
cout << setprecision(8) << showpoint;
cout << "S showpoint: " << f << endl;
cout << "Nastaveni scientfic: " << scientific << f << endl;
cout << "Zpet na fixed: " << fixed << f << endl;
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad4).
Výstup:
Bez nastaveni: 2
S showpoint: 2.0000000
Nastaveni scientfic: 2.00000000e+000
Zpet na fixed: 2.00000000
Manipulátor fixed je nastaven standardně a určuje zápis čísla jen desetinnou čárkou, scientific pak umožňuje výpis ve tvaru s exponentem. Manipulátor showpoint pak říká, že chceme vidět čísla za desetinnou čárkou i v případě, že to je 0.
Další vlastnosti se dají nastavit pomocí metody setiosflags(), mazat se pak dají pomocí metody resetiosflags().
Nyní si ukážeme, jak napsat vlastní manipulátor:
#include <iostream>
using namespace std;
ostream& DveRadky(ostream& vystup)
{
return vystup << '\n' << '\n' << '\n';
}
int main(int argc, char* argv[])
{
cout << "Nasleduji dve prazdne radky: " << DveRadky << "Konec vystupu" << endl;
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad5).
Náš upravený manipulátor musí vracet referenci na objekt výstupního proudu, aby se dal řetězit jako ostatní operátory. Podobně lze vytvořit i operátor pro vstupní proud, stačí místo ostream použít třídu istream.
Jak jsme si uvedli pro výstup do souboru slouží třída ofstream. My se však rozhodneme pro jejího dokonalejšího potomka, vstupně-výstupní třídu fstream, kterou otevřeme pro zápis, podobně jako jsme to dělali v C pomocí funkce fopen(). Nejlepší bude, ukázat si vše na příkladu:
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
fstream soubor("data.dat", ios::out);
if(soubor.is_open())
{
soubor << "Nazdar svete diskoveho prostoru!!!" << endl;
soubor.close();
}
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad6).
Vidíme, že postup je velice jednoduchý. Jméno souboru a způsob přístupu (existuje jich samozřejmě celá řada) k souboru zadáme v konstruktoru, pak jen ověříme, zda-li otevření proběhlo úspěšně a můžeme zapisovat. Po práci samozřejmě soubor uzavřeme. Zde zmíníme, že pokud dojde k vyvolání výjimky v programu, pak se standardní proudy samy uzavřou v destruktorech. V reálném kódu by samozřejmě měla být doplněna vhodná reakce na chybu při otevření souboru.
Podívejme se na čtení ze souboru. Můžeme použít opět vstupně-výstupní třídu fstream (pokud ji otevřeme pro čtení), ale tentokrát zkusíme třídu ifstream. Abychom měli co číst, použijeme i třídu ofstream pro zápis nějakých dat. Příklad:
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
ofstream out("data.dat");
if(out.is_open())
{
for(unsigned char a = 27; a < 255; a++)
{
out << a; // Zapiseme ASCII tabulku (bez problemovych znaku)
}
out.close();
ifstream in("data.dat");
if(in.is_open())
{
char c;
while(in >> c)
{
cout << c;
}
in.close();
}
}
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad7).
Vidíme, že vstup není o moc složitější. Program by se samozřejmě dal upravit, aby používal jen jednu proměnnou typu fstream, stačí ho před druhým použitím uzavřít a znovu otevřít pomocí metody open() . Opět chybí reakce na chyby při otevření souborů.
Ještě je nutné uvědomit si, s jakými daty pracujeme. Pokud jsou v souboru binární data, pak použitím operátorů << a >> dojde ke ztrátě některých BYTE. Pro otevření proudu jako binární ho musíme otevřít takto (implicitně se otevírá v textovém módu):
fstream soubor("data.dat", ios::out || ios::binary);
Pro vstup a výstup je pak třeba použít metody istream::read(), resp. ostream::write(). Tyto metody se používají následovně:
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
ofstream out("data.dat", ios::binary);
if(out.is_open())
{
for(unsigned char a = 0; a < 255; a++)
{
out.write((char*)&a, sizeof(a)); // Zapiseme ASCII tabulku
}
out.close();
ifstream in("data.dat", ios::binary);
if(in.is_open())
{
char c;
while(in.read((char *)&c, sizeof(c)))
{
cout << c;
}
in.close();
}
}
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad8).
Pro porovnání zkuste v příkladu, kde se používají klasické operátory pro vstup a výstup, změnit hodnotu proměnné a na 0, program neproběhne v pořádku.
Pro standardní datové typy jsou operátory pro vstup a výstup přetíženy uvnitř třídy istream (nebo ostream). Protože to už jsou hotové třídy, nemůžeme naše metody do ní přidat, musíme tedy přetížit operátorové funkce pro naše typy a využít klíčového slova friend:
#include <iostream.h>
// pokud tu je iostream + using namespace
std, tak msvc6 hlasi chyby
class Souradnice {
private:
int m_ix, m_iy;
public:
Souradnice(int _ix = 0, int _iy = 0) : m_ix(_ix), m_iy(_iy) { ; }
friend istream& operator>>(istream& p, Souradnice& s);
friend ostream& operator<<(ostream& p, Souradnice& s);
};
istream& operator>>(istream& p, Souradnice& s)
{
return p >> s.m_ix >> s.m_iy;
}
ostream& operator<<(ostream& p, Souradnice& s)
{
return p << "[" << s.m_ix << "," << s.m_iy << "]";
}
int main(int argc, char* argv[])
{
return 0;
}
Kód naleznete v sekci Downloads (projekt Priklad9).
Operátory pro vstup a výstup zase vrací příslušné proudy a jdou tedy řetězit. Pokud bychom měli celou hierarchii tříd, pak je vhodnější definovat pro rodičovskou třídu operátory pro vstup a výstup a z jejich těla pak volat virtuální metody pro vstup, resp. výstup.
Jak jsme naznačili v úvodu, tak tento článek o C++ byl prozatím poslední. V příštím článku se budeme věnovat datovým strukturám - od jednoduchých proměnných, přes lineární spojové seznamy až po stromy. Až tohle dokončíme, tak se vrátíme k standardní šablonové knihovně jazyka C++, která nám usnadňuje operace s některými datovými strukturami.
Příště nashledanou.