Lekce 3 - Proudy v C++
V minulé lekci, Typy souborů a správné umístění souborů v C++, jsme si řekli základní informace pro práci se soubory různých formátů na různých operačních systémech.
Dnes si konečně vysvětlíme, co to jsou proudy a k čemu jsou nám dobré.
Co je to proud
Proud je sekvence dat, která lineárně zpracováváme. Synonymem proudu by
mohl být například tok dat. Vezměme si výpis do konzole. O tom jsme si
řekli, že je proud. A kde je to "lineární zpracování"? Data se vypisují
tak, jak je zapisujeme. Znak po znaku přidáváme znaky do proudu, který
"odtéká" do konzole. Stejně je tomu u objektu cin
. Tam pro
změnu data "přitékají" z konzole a my je čteme. Například chceme-li
přečíst číslo, vybereme z proudu znaky reprezentující číslici. Tyto
znaky následně zpracujeme (tedy naparsujeme) a výsledkem je číslo.
Jaké by byly další příklady proudů? Například pokud pracujeme s mikrofonem. Z okolí budeme nahrávat zvuk, který je opět pouze proud dat, který naše aplikace zpracovává. Dále například video. Zde nám dokonce figurují dva proudy. Jedním proudem čteme data ze souboru, která aplikace následně zpracovává. Druhým proudem zpracovaná data "vykreslujeme" na obrazovku jako obrázky, a tím získáme video.
Sjednocení práce s daty
Myšlenka proudů (v anglické literatuře streams) je velmi silná. V aplikaci nás nemusí zajímat, odkud data pochází. Například v případě videa můžeme data číst ze souboru, ze sítě, z kamery nebo dokonce můžeme snímat obrazovku a posílat data zpět. Jádro aplikace se v takovém případě vůbec nemění, protože díky proudům je přístup k datům jednotný.
Typy proudů
Když již víme, co to proudy jsou, můžeme si je nyní lépe rozdělit. Na začátku jsme si řekli, že data mohou "přitékat" a také "odtékat". Tím jsou proudy rozděleny na dvě základní kategorie - vstupní a výstupní.
Vstupní proudy dědí ze třídy istream
, kterou najdeme v
hlavičkovém souboru istream
. Třída istream
poskytuje rozhraní (API), které jsme viděli a používali v první lekci.
Výstupní proudy dědí ze třídy ostream
, která je
definovaná v hlavičkovém souboru ostream
. Její použití jsme
viděli nejen v první lekci, ale ve všech tutoriálech, protože právě
objekt cout
dědí ze třídy ostream
.
Některé proudy mohou být samozřejmě obojí (tedy jak vstupní tak
výstupní). Takovým příkladem je například třída fstream
.
Můžeme soubor otevřít pro čtení i zápis, nějaké části přečíst a
nějaké části přepsat. Musíte ovšem počítat s tím, že při zápisu se
budou data přepisovat.
Seekable a non-seekable proudy
Proudy dále dělíme na tzv. seekable streams (volně přeloženo asi jako prohledatelné proudy) a non-seekable streams (neprohledatelné či čistě lineární proudy). Protože čeština nemá skutečný překlad těchto termínů, budu i dále používat jejich anglické názvy.
Seekable proudy
Seekable proudy jsou proudy, ve kterých se můžeme přesouvat. Například když máme data v paměti a víme, že jsme na offsetu 64, je pro nás jednoduché se přesunout na index 32 (pouze přesuneme ukazatel). Stejně tak, pokud čteme 100. bajt souboru a najednou chceme přečíst 50. bajt, můžeme se v souboru přesunout pouze tím, že řekneme operačnímu systému, aby nám vrátil 50. bajt souboru. Seekable proudy jsou zpravidla takové, kde jsou data již někde zaznamenána. Pokud se v takových datech někam přesuneme, neztrácíme žádné informace, protože ta jsou uložena někde jinde.
Non-seekable proudy
Na druhé straně jsou non-seekable proudy, které můžeme
číst (resp. zapisovat) pouze od začátku do konce.
Pokud některá data přeskočíme (například použitím metody
ignore()
), jsou poté tato data nenávratně ztracena a již se k
nim nedostaneme. Stejně tak se nedostaneme k již přečteným datům, protože
ta jsou již zpracována. Typickým příkladem je právě konzole. Jakmile na
výstup něco vypíšeme, nelze (standardními prostředky) vypsaný text
smazat. Operační systém samozřejmě tyto prostředky má, ale musíte
použít systémová volání a v této chvíli pouze program využívá faktu,
že si operační systém výstup pamatuje. Starší zařízení, která tyto
vlastnosti neměla, již text upravit nedokázala. Dalším příkladem
non-seekable proudu je na příklad poslech mikrofonu nebo čtení dat ze
sítě. Jakmile jsou data zpracována a vyrovnávací paměť proudu je
vymazána, není je již možné získat.
Nyní se vrátíme k seekable proudům. Vzhledem k tomu, že proudy mohou
být i vstupní i výstupní, nevystačíme si pouze s metodou
seek()
, ale musíme rozlišit pozici pro čtení a pozici pro
zápis. Metody pracující s pozicí pro zápis jsou ukončeny písmenem
p (od slova put) a metody pracující s pozicí pro čtení jsou
ukončeny písmenem g (od slova get). Metody máme k dispozici
následující:
tellg()
- vrátí aktuální pozici pro čtení od začátku proudu (tedy absolutní pozici)tellp()
- vrátí aktuální pozici pro zápis od začátku proudu (tedy absolutní pozici)seekg( pos_type pos )
- nastaví pozici pro čtení absolutně (tj. od začátku proudu); volánímproud.seekg(proud.tellg())
zůstane pozice nezměněnaseekg( off_type off, std::ios_base::seekdir dir)
- nastaví pozici pro čtení relativně, a to vzhledem k použitému flagu, který se předává jako druhý parametrseekp( pos_type pos )
- nastaví pozici pro zápis absolutně (tj. od začátku proudu)seekp( off_type off, std::ios_base::seekdir dir)
- nastaví pozici pro zápis relativně, a to vzhledem k použitému flagu, který se předává jako druhý parametr
Hodnotou druhého parametru metod seekx()
může být:
ios:beg
- od začátku proudu (tedy stejný případ jako předchozí přetížení)ios:end
- od konce proudu (pozice musí být záporná nebo0
)ios:cur
- od aktuální pozice
Pojďme si to nyní vyzkoušet na souboru:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream soubor1("zapis.txt", ios::out);
soubor1 << "Hello World!" << endl;
soubor1.close();
fstream soubor("zapis.txt");
//precteni slova
string slovo;
char radek[128];
soubor >> slovo;
cout << "Po precteni slova \"" << slovo << "\" je pozice " << soubor.tellg() << " pro cteni a " << soubor.tellp() << " pro zapis" << endl;
//prepsani souboru
soubor.seekp(0);
soubor << "Ya Ya";
soubor.seekg(0);
soubor.getline(radek, 128);
cout << "Prvni radek souboru: " << radek << endl;
//zapis na konec
soubor.seekp(-1, ios::end);
soubor << " You are so awesome";
soubor.seekg(0, ios::beg);
soubor.getline(radek, 128);
cout << "Prvni radek po zmene souboru: " << radek << endl;
return 0;
}
Standardní proudy
Již jsme se dozvěděli o objektech cout
a cin
,
které slouží jako proudy pro vstup a výstup z konzole.
istream, ostream a iostream
Dále víme o třídách istream
a ostream
, které
slouží jako bázové třídy pro ostatní proudy.
istream
poskytuje rozhraní pro vstupní proudy, zatímco
ostream
poskytuje rozhraní pro proudy výstupní. Dále existuje
třída iostream
, která (jak je z názvu patrné) poskytuje
rozhraní pro vstupní i výstupní proudy. Reálně třída
iostream
dědí ze tříd istream
a
ostream
.
ifstream, ofstream a fstream
Pro práci se soubory jsme použili třídu fstream
, která
dědí z iostream
a poskytuje tak obě rozhraní. Toto rozhraní
můžeme nicméně rozložit a tak existují ve standardu dále třídy
ifstream
(vstupní proud pro soubory) a ofstream
(výstupní proud pro soubory). V tomto případě již neplatí, že třída
fstream
dědí ze tříd ifstream
a
ofstream
, ale pouze ze třídy iostream
.
istringstream, ostringstream, stringstream
Standard dále definuje třídu stringstream
v knihovně
sstream
, která ukládá data v operační paměti
RAM. Tento proud můžeme použít pro předávání dat uvnitř aplikace. V
souvislosti s použitím čistě RAM paměti je potřeba dát pozor na jeho
velikost. Pokud čteme soubor pomocí třídy fstream
, soubor je
čten po částech a nenačítá se do operační paměti. Pokud načteme velký
soubor (například 4GB) a uložíme jeho obsah do objektu třídy
stringstream
, program zabere minimálně 4GB operační
paměti.
Stejně jako třída fstream
má i třída
stringstream
varianty pouze pro čtení
(istringstream
) nebo pouze pro zápis (ostringstream
).
Data pro istringstream
naplníme při jeho vytváření. Třídu
ostringstream
přirozeně plnit nemusíme, protože do ní teprve
data budeme předávat. Data poté získáme metodou str()
, která
vrátí obsah jako string
. Pokud zavoláme metodu
str()
s parametrem typu string
, je vnitřní obsah
proudu nahrazen obsahem parametru. Toho lze využít i pro
istringstream
- pokud je proud prázdný, dodáme další data a
zbytek aplikace může s proudem dále pracovat.
Hlavní použití stringstream
třídy je pro převod objektů
na řetězec. C++ nemá nic jako metodu toString()
. Pro výstup se
používá operátor <<
, jako jsme to dělali u našich
tříd. Pomocí stringstream
můžeme převést objekt na řetězec
stejně, jako bychom objekt vypisovali:
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
using namespace std;
class MojeTrida{};
ostream& operator<<(ostream& str, const MojeTrida& trida)
{
return str << "Reprezentace tridy";
}
int main()
{
MojeTrida a;
ostringstream str;
str << a;
string prevedenoNaRetezec = str.str();
cout << "Vypis tridy jako retezec: " << prevedenoNaRetezec << endl;
return 0;
}
Shrnutí
Výjimečně si dovolím krátké shrnutí dnešní lekce.
- Proud či stream je sekvence bajtů, ze které čteme nebo do které zapisujeme.
- Proudy dělíme na vstupní (dědící ze třídy
istream
) a výstupní (dědící ze třídyostream
). - Proudy mohou být seekable a non-seekable.
stringstream
slouží jako náhradatoString()
metody.
Nakonec přikládám kompletní hierarchii zmíněných tříd v C++.
To je pro dnešní lekci vše.
V té příští, Souborové proudy v C++ a UTF kódování, se ještě jednou podíváme na souborové proudy a naučíme se pracovat s rozšířenou znakovou sadou.