Lekce 1 - Úvod do práce se soubory v jazyce C++
Vítejte u první lekce kurzu tutoriálů o práci se soubory a proudy v jazyce C++.
K čemu potřebujeme soubory?
Dosud jsme vždy pracovali pouze se standardním vstupem a výstupem (tedy s
objekty cout
a cin
). Veškerá data jsme měli
uložená v paměti
RAM. To má nespornou výhodu v rychlosti jejich čtení, ovšem po
ukončení aplikace jsou všechna data z paměti smazána a tím ztracena. Pokud
naši aplikaci spustíme znovu, k datům se již nedostaneme (protože již ani
neexistují). Zde přichází na řadu soubory. Do souborů si můžeme vložit
libovolnou informaci a při příštím spuštěním aplikace ji opět
načíst.
V C++ je práce se soubory součástí většího celku, který se nazývá proudy. Co to jsou proudy a jak s nimi pracovat si povíme později. V této lekci se podíváme na základní práci se soubory.
Základní operace
Pro práci se soubory má C++ dvě API. Jedno API je v hlavičce cstdio
a
jedná se o API zděděné z jazyka C. Toto API je
popsáno v kurzu Práce se soubory v jazyce
C a nebudeme se jím nadále zabývat. Čistě C++ API pro práci se soubory
nabízí knihovna fstream
. V ní je (kromě jiných tříd) třída
fstream
, se kterou budeme dnes pracovat.
API je zkratka pro Application
Programming Interface. To se dá přeložit
jako "rozhraní pro programování aplikací" a jedná se o souhrn funkcí a
tříd, které může programátor využívat. Například napíšete-li
knihovnu pro sečtení dvou čísel, její rozhraní bude např. funkce
double secti(double a, double b)
. Každá knihovna poskytuje
nějaké rozhraní, v opačném případě by nebyl způsob jak s ní pracovat.
To, že můžeme vypisovat do konzole jako cout << neco
, je
opět pouze API, které poskytuje objekt cout
(nebo obecněji
standardní knihovna).
Otevření souboru pro zápis
Soubor nejdříve musíme otevřít (či vytvořit, pokud neexistuje). To
provádíme konstruktorem objektu, který přijímá název souboru jako
parametr. Pokud chceme soubor i vytvořit, předáme jako druhý parametr
konstruktoru hodnotu ios::out
. Poté ze souboru můžeme číst
nebo do něj zapisovat. Následuje uvolnění prostředků pomocí metody
close()
.
Ukažme si první příklad, který otevře soubor pro zápis:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream soubor("zapis.txt", ios::out);
// operace se souborem
//nemusi byt volano, bude zavolano po destruovani objektu
soubor.close();
cin.get();
return 0;
}
Pokud bychom objekt neuzavřeli, soubor zůstane v držení
aplikace. V takovém případě vám operační systém neumožní se souborem
manipulovat (například jej smazat), dokud není soubor uvolněn. Jedná se o
klasickou hlášku "Soubor nelze smazat, protože je používán jiným
procesem". Nutno podotknout, že třída fstream
provádí
uzavření automaticky v destruktoru, metodu close()
je tedy nutné
volat jen pro objekty, jejichž platnost je delší než je jejich použití
(například na začátku metody main()
).
Zápis do souboru
Umíme otevřít již existující soubor pro zápis. A jak že do něj
vlastně zapisovat? Překvapivě stejně, jako jsme to dělali s objektem
cout
. Objekt cout
je totiž podobně jako třída
fstream
pouze proud. Pojďme tedy vypsat do souboru text "Hello
World":
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream soubor("zapis.txt", ios::out);
soubor << "Hello World" << endl;
//nemusi byt volano, bude zavolano po destruovani objektu
soubor.close();
cin.get();
return 0;
}
A pokud chceme se souboru něco přečíst? Opět použijeme to, co už
známe. Čtení probíhá stejně jako u objektu cin
, který je
(překvapivě) opět pouze proud. Načtěme text, který jsme do souboru zapsali
výše:
#include <iostream> #include <fstream> #include <string> using namespace std; int main() { fstream soubor("zapis.txt"); string slovo; soubor >> slovo; cout << slovo << endl; //nemusi byt volano, bude zavolano po destruovani objektu soubor.close(); cin.get(); return 0; }
Hello World
Nyní již umíme základní operace. To ale není vše. Proudy toho umí mnohem více a teď si představíme jejich základní metody.
Neformátovaný vstup a výstup
Vše, co jsme zatím prováděli, jsme prováděli na tzv. formátovaném
vstupu, resp. výstupu. Proč mluvíme o formátovaném? Pokud jsme chtěli
vypsat například číslo, pouze jsme do cout
poslali
int
a cout
se už postaral o správný převod čísla
na text. Jinými slovy námi předaná data zformátoval do
čitelné podoby. Někdy kvůli výkonu chceme formátování přeskočit a
vypsat pouze text. O to se stará právě neformátovaný vstup, resp.
výstup.
Neformátovaný výstup
Pro výstup jsou to dvě metody, put()
a write()
.
Ta první předá na výstup pouze jeden znak, zatímco metoda
write()
vypíše pole. Proč bychom chtěli používat
neformátovaný výstup? Protože je rychlejší než výstup formátovaný. Pro
neformátovaný výstup neprobíhá žádná konverze, žádná kontrola, obsah
pole je pouze zkopírován.
Neformátovaný vstup
Pro neformátovaný vstup je metod více. Základem je metoda
get()
, která ze vstupu odebere jeden znak. Existuje i
přetížení, které ze vstupu odebere znaků několik.
int get()
vrátí znak ze vstupu.istream& get (char& c)
vrátí znak ze vstupu argumentem.istream& get (char* s, streamsize n)
vrátí až n-1 znaků ze vstupu a uloží je do poles
. Funkce skončí po přečtení n-1 znaků nebo po výskytu znaku\n
na vstupu (ten ve výstupu zůstává). Na konec pole je automaticky přidán znak\0
a jedná se tedy o plnohodnotný C-like řetězec (proto čte max n-1 znaků).istream& get (char* s, streamsize n, char delim)
stejný jako předchozí případ, pouze lze namísto ukončovacího symbolu\n
zvolit cokoliv jiného.istream& get (streambuf& sb)
přečte vstup až po znak\n
a vloží jej do streambufferu (viz. další lekce).istream& get (streambuf& sb, char delim)
přečte vstup až po znakdelim
a vloží jej do streambuferu (viz. další lekce).
Vstup má dále metody read()
a getline()
, které
fungují téměř stejně jako třetí případ metody get()
.
Metoda read()
pouze přečte n znaků nebo selže
(nepřidává \0
znak). Metoda getline()
přečte
maximálně n-1 znaků, přidá \0
symbol a ukončovací
symbol delim
odstraní ze vstupu, pokud na něj narazila. K tomu,
jak vstup může selhat, se dostaneme někdy jindy.
Nahlížení
Kromě samotného čtení můžete na znaky pouze nahlížet, a to metodou
peek()
. Ta vrátí další znak ze vstupu, ale neodstraní jej
(takže zůstane pro další čtení k dispozici).
Přidávání
Na výstup můžete dokonce i znaky přidávat, a to metodou
putback()
. Ta vloží jeden znak do fronty pro čtení. To se
samozřejmě neprojeví v konzoli (text, který uživatel napsal, tam zůstane),
ale při dalším čtení bude znak přečten.
Ignorování
Nakonec tu máme metody pro ignorování vstupu, tj. metodu
ignore()
. Ta je deklarovaná jako
istream& ignore (streamsize n = 1, int delim = EOF)
. Parametr
n
udává počet znaků k odstranění (-1 znamená bez limitu) a
delim
definuje znak, kde se má metoda zastavit. Konstanta
EOF
označuje konec souboru (taktéž bude probráno dále).
Následující program demonstruje základní neformátované operace:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
//{
fstream zapis("soubor.txt", ios::out);
zapis.write("Hello World", 11);
zapis.put('!');
zapis.put('?');
zapis.close();
//}
fstream cteni("soubor.txt", ios::in);
cout << "Dalsi znak: " << (char)cteni.peek() << endl;
char prvni = (char)cteni.get();
char druhy = (char)cteni.get();
cout << "Po znaku " << prvni << " je znak " << druhy << endl;
char zbytekHello[4];
cteni.get(zbytekHello, 4, ' ');
cout << "Zbytek slova je " << zbytekHello << endl;
cteni.ignore(999, 'l');
cout << "Dalsi znak po prvnim ignorovani: " << (char)cteni.peek() << endl;
cteni.ignore(1);
char zbytekRadku[3];
cteni.getline(zbytekRadku, 3);
cout << "Po ignorovani je zbytek radku " << zbytekRadku << endl;
cteni.close();
cin.get();
return 0;
}
Všimněte si explicitního uzavření prvního souboru. Pokud by první
objekt nebyl uzavřen, druhý by z něj nemohl číst, protože soubor může
být otevřen vždy jen jednou. Nicméně pokud by byl první blok kódu
uzavřen do složených závorek, potom se na jejich konci zavolá destruktor
objektu zapis
a tak můžeme volání metody close()
vynechat.
Kompletní výklad, co druhý parametr konstruktoru ios::out
dělá, si necháme na někdy jindy.
Tím máme základy za sebou.
Příště, v lekci Typy souborů a správné umístění souborů v C++, se podíváme na různé typy souborů a jak s nimi nakládat.