Kurz C++ (8.)
Práce se soubory
Základy
Dnešní díl kurzu C++ je věnován práci se soubory. Říkám-li C++, dopouštím se dnes
jisté mystifikace, my se budeme zatím učit způsob, který existoval již v jazyce
C, přesněji řečeno v jeho run-time knihovně. Jazyk C++ přinesl objekty a
objektově orientovaný přístup k souborům jistě nemohl chybět, ale my zatím
objekty neumíme. Až se je naučíme, vysvětlíme si i přístup "po C++".
Veškeré funkce a struktury potřebné pro práci se soubory se nachází v
hlavičkovém souboru <stdio.h> , nezapomínejte ho pokaždé
vkládat. Funkce v něm deklarované používají tzv. proudy. Proud
si můžete představit jako nějakou datovou strukturu, která slouží k přístup k
obsahu souboru a provádění všech možných operací. Také udržuje aktuální polohu
v souboru (aktuální poloha se mění čtením ze souboru a zápisem do něj), a také
poskytuje vyrovnávací paměti (to má za úkol zrychlovat přístup k souboru,
trochu více si o tom vysvětlíme dále). Ještě bych chtěl podotknout, že pro plné
pochopení látky bude potřeba pracovat s nápovědou. Funkcí pro práci se soubory
je mnoho a jejich možnosti skutečně rozsáhlé, proto není zde místo popsat vše
(to by vydalo na knihu).
Otevření a zavření
Na začátku práce se souborem je třeba ho otevřít (tím se vytvoří proud) a k tomu
slouží funkce fopen() , která je deklarovaná takto:
FILE *fopen(const char *filename, const char *mode);
To znamená, že funkce fopen() dostává jako parametry dva řetězce:
první určuje cestu k souboru (může být relativní i absolutní), a druhý určuje,
jak se má soubor otevřít (zda jen pro čtení, nebo jen pro zápis, popř. pro
čtení i zápis najednou, a dále zda se má vytvořit nový soubor pokud cesta
zadaná první parametrem neexistuje - vše si vysvětlíme). Velice důležitá je
návratová hodnota funkce fopen() , je to ukazatel na proměnnou typu
FILE (struktura FILE je také deklarovaná v <stdio.h> ).
Tato proměnná je alokována někde v útrobí run-time knihovny a my dostáváme
pouze ukazatel, který si musíme někam uschovat, protože pomocí něho budeme
provádět veškeré další operace se souborem. Dále hned po zavolání funkce fopen()
je třeba testovat vracený ukazatel, zda není nulový - to by znamenalo, že došlo
k nějaké chybě a soubor se nepodařilo otevřít:
FILE *proud;
proud = fopen("soubor.txt", "r");
if (!proud)
cout << "Soubor se nepodarilo otevrit";
Použili jsme pro parametr mode hodnotu "r" , která
znamená, že soubor má být otevřen pouze pro čtení. Další možné hodnoty jsou:
r |
pouze čtení |
w |
pouze zápis (přepíše existující soubor, vytvoří nový pokud soubor neexistuje) |
a |
zápis pouze na konec souboru (přidávání) |
r+ |
čtení i zápis |
w+ |
jako w, ale i čtení |
a+ |
jako a, ale i čtení |
Na konec řetězce mode je možné přidat jeden z těchto znaků:
t |
textový mód - při čtení překládá znakové kombinace CR/LF (znaky s kódy 13/10
znamenající konec řádku) na LF (znak s kódem 10 odpovídající escape-sekvenci
\n),
při zápisu překladá LF na CR/LF - tím je zjednodušena kontrola konců řádků v
textovém souboru (není potřeba testovat dva znaky za sebou, ale pouze jeden)
|
b |
binární mód - neprobíhá žádný překlad |
Textový mód použijeme pokud zpracováváme textové soubory, ve všech jiných
případech použijeme binární mód. Pokud se neuvede ani b ani t platí textový
mód, ale toto lze také měnit nastavení globální proměnné _fmode ,
deklarované v hlavičkovém souboru <stdlib.h> .
Poznámka: uvedení znaku b bude určitě fungovat ve Visual C++, ale nemusí
fungovat v jiných překladačích nebo na jiných operačních systémech. Myslím, že
norma uvádí pouze znak t, ale nemohu to zjistit protože ji nemám k dispozici
(nedá se sehnat v elektronické podobě).
Na konci práce se souborem je potřeba ho uzavřít. Pokud ho zapomeneme uzavřít,
run-time knihovna ho uzavře za nás, ale s tím nesmíme počítat, uklízet po sobě
patří k základním programátorským zvykům. Soubor se zavírá funkcí fclose() :
int fclose(FILE *stream);
Funkce vrací 0 pokud uzavření proběhlo v pořádku nebo konstantu
EOF pokud došlo
k nějaké chybě.
Čtení ze souboru
Jazyk C poskytuje mnoho možností čtení ze souboru. Je možné číst jednotlivé
znaky, řádky, nebo i libovolný počet znaků najednou. Například:
int fgetc(FILE *stream);
Přecte jediný znak, vrací EOF v případě chyby nebo konce souboru.
char *fgets(char *string, int n, FILE *stream);
Přečte celý řádek (do nalezení znaku \n ) včetně \n ,
ale ne více než n znaků (včetně nulového ukončovacího, takže se ze
souboru přečte pouze nejvýše n - 1 znaků).
Tyto dvě funkce jsou spíše vhodné pro textové soubory. Pro binární soubory (kde
chceme číst určitý počet znaků najednou) se hodí funkce fread() :
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
Tato funkce přečte z proudu stream count položek
velikosti size , které uloží do paměti na místo, kam ukazuje
ukazatel buffer (všimněte si, že je typu void , takže
tímto parametrem můžete předávat ukazatel na jakýkoli datový typ). Funkce vrací
počet skutečně přečtených položek (toto číslo může být menší nez count
v případě chyby).
Nyní si ukážeme malý příklad: chceme spočítat počet řádků v textovém souboru.
Nabízí se možnost přečíst celý soubor znak po znaku a počítat znaky \n :
#include <stdio.h>
#include <iostream.h>
void main(int argc, char *argv[]) {
FILE *proud;
char ch;
unsigned radky =
0; if (argc
< 2) { cout
<< "Zadejte soubor jako parametr.\n";
return;
}
// otevrení souboru v textovém módu pro čtení
proud = fopen(argv[1], "rb");
if (!proud)
return;
// přečtení prvního znaku
ch = fgetc(proud);
// dokud není konec souboru
while (ch != EOF) {
if (ch == '\n')
radky++;
// čteme další znak
ch = fgetc(proud);
}
cout << "Pocet radku: " << radky + 1 << endl;
}
Zápis do souboru
Funkce pro zápis do souboru mají podobná jména jako ty pro čtení, ale místo
"get" se používá "put" a místo "read" je "write".
int fputc(int c, FILE *stream);
Zapíše znak c a vrací hodnotu c v případě úspěchu, EOF
jinak.
int fputs(const char *string, FILE *stream);
Zapíše do souboru řetězec string
(bez ukončovacího znaku), vrací kladnou hodnotu v případě úspěchu a
jinak EOF.
Ekvivalent funkce fread() je funkce fwrite() :
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);
Tato funkce zapíše do proudu stream count položek
velikosti size , které přečte z paměti z adresy buffer .
Návratová hodnota je počet zapsaných položek (pozor, ne bytů).
Vyrovnavací paměti
Velkou výhodou proudů je že veškeré čtecí a zapisovací operace probíhají přes
vyrovnávací paměti. V případě čtení to vypadá tak, že v okamžiku kdy je vyžádán
první znak souboru je toho přečteno více (např. dokumentace VC++ 6.0 uvádí 4
KB), a další čtení proběhne pouze z vyrovnávací pamětí, bez přístupu na disk
(ten se samozřejmě provede, když už nejsou další data ve vyrovnávací paměti).
Podobně zápis se provádí do vyrovnávací paměti, která se zapíše na disk až v
okamžiku naplnění. Přínos vyrovnávacích pamětí je mnohonásobně rychlejší čtení
i zápis. Pokud si to chcete vyzkoušet, přidejte v našem čítači řádků hned
pod volaní fopen () toto:
setvbuf(proud, 0, _IONBF, 0);
Tím se vypínají vyrovnávací paměti. Zkuste spustit program na nějaký větší
soubor (alespoň 1 MB) a uvidíte rozdíl.
Další funkce
Pro skutečně efektivní práci se soubory si nevystačíme pouze s funkcemi pro
zápis a čtení. Existují další funkce jako například:
int fflush(FILE *stream);
Slouží k vyprázdnění vyrovnávacích pamětí. U proudů otevřených pro čtení
vyprázdní vyrovnávací paměť, u proudu otevřených pro zápis provede totéž, ale
předtím zapíše obsah vyrovnávací paměti na disk. Tato funkce je obzvlášť
užitečná u souborů otevřených pro čtení i zápis, protože mezi přepínáním mezi
čtením a zápisem musíme přidat volání funkce fflush()
(z důvodu použití vyrovnávací paměti).
long ftell(FILE *stream);
Vrací aktuální polohu v souboru (je to celočíselná hodnota která udává na jakém
místě v souboru proběhne další operace čtení nebo zápisu - hned po otevření má
hodnotu nula a zvyšuje se s každou operací čtení nebo zápisu).
int fseek(FILE *stream, long offset, int origin);
Nastavuje aktuální polohu v souboru na offset bytů od místa,
udávaného parametrem origin . Parametr origin nabývá hodnot: SEEK_CUR
(aktuální poloha), SEEK_END (konec souboru), SEEK_SET
(konec souboru). Například
// nastavujeme polohu 10 bytů od začátku souboru
fseek(proud, 10, SEEK_SET);
// nastavujeme polohu 10 bytu od konce souboru (pozor na zápornou hodnotu!)
fseet(proud, -10, SEEK_END);
Funkce fseek() vrací 0 v případě úspěchu a
jinak jinou hodnotu.
Bohužel funkce fseek() má omezenou použitelnost u souborů
otevřených v textovém módu – lze ji použít pouze volání s origin = SEEK_SET
a navíc parametr offset musí obsahovat hodnotu vracenou funkcí ftell() .
void rewind(FILE *stream);
Nastaví aktuální polohu v souboru na jeho začátek.
int fscanf(FILE *stream, const char *format [, argument ]...);
int fprintf(FILE *stream, const char *format [, argument ]...);
Tyto funkce jsou obdobné funkcím scanf() a printf() .
Speciální proudy
Run-time knihovna definuje takzvané standardní proudy, které jsou při spuštění
programu automaticky otevřeny. Jsou definovány tři standardy proudy, které
můžete použít v programech podobně jako jsme výše použili např. proměnnou proud :
stdin |
je pevně nastaven na tzv. standardní vstup, což je většinou klávesnice |
stdout |
je pevně nastaven na standardní výstup, což bývá monitor |
stderr |
jedná se o tzv. standardní chybový výstup, který je určen pro vypisování chyb,
a většinou je také přesměrován na monitor |
To znamená, že budete-li číst ze stdin , provádíte vlastně čtení
vstupu z klávesnice, a obdobně zápis na stdout znamená zápis na
obrazovku.
Příklad
Jako větší příklad si dnes doplníme adresář, který jsme napsali minule, o práci
se soubory. Jmenovitě budeme potřebovat dvě funkce, nazvěme je uloz()
a otevri() .
Ukládat budeme celé struktury Osoba , a to přesně tolik, kolik je
hodnota osoby.pocet . Ale abychom mohli pak uložený soubor nahrát
zpátky, potřebujeme vědět, kolik osob v souboru vlastně je. Proto jako první
uložíme hodnotu proměnné osoby.pocet a to binárně, tzn. uložíme 4
byty, které tuto proměnnou tvoří. Dále budeme muset počítat s chybami při
zápisu. Pokud se nepodaří otevřít soubor pro zápis, prostě vracíme false .
Ale v případě, že jsme soubor úspěšně otevřeli, a dojde k chybě při zápisu,
bude třeba ho uzavřít a vymazat. Proto si vytvoříme ještě
jednu funkci, chybaUlozeni() , která tyto činnosti provede:
const char *strModUloz = "wb";
bool uloz(Osoby &osoby, char *soubor) {
FILE *fp;
fp = fopen(soubor, strModUloz);
if (!fp)
return false;
// zapisujeme počet osob
if (fwrite(&osoby.pocet, sizeof(osoby.pocet), 1, fp) != 1)
return chybaUlozeni(fp, soubor);
// zapisujeme struktury Osoba
// přetypovani je nutné kvůli tomu, že osoby.pocet je unsigned,
// ale návratová hodnota fwrite je int
if (fwrite(osoby.pole, sizeof(*osoby.pole), osoby.pocet, fp) != (size_t)osoby.pocet)
return chybaUlozeni(fp, soubor);
fclose(fp);
return true;
}
bool chybaUlozeni(FILE *fp, char *soubor) {
fclose(fp);
// funkce remove se take nachazi v <stdio.h>
remove(soubor);
return false;
}
Otevření provedeme podobně, s tím rozdílem, že pokud dojde k nějaké chybě,
soubor zavřeme a nastavíme osoby.pocet = 0 :
const char *strModOtevri = "rb";
bool otevri(Osoby &osoby, char *soubor) {
FILE *fp;
osoby.pocet = 0;
fp = fopen(soubor, strModOtevri);
if (!fp)
return false;
// čteme počet osob
if (fread(&osoby.pocet, sizeof(osoby.pocet), 1, fp) != 1)
return chybaOtevreni(fp, osoby);
// pokud je počet osob vetší než zvládneme, hlásíme chybu
if
(osoby.pocet > MAX_POCET_OSOB)
return chybaOtevreni(fp, osoby);
// čteme osoby
if (fread(osoby.pole, sizeof(*osoby.pole), osoby.pocet, fp) != (size_t)osoby.pocet)
return chybaOtevreni(fp, osoby);
fclose(fp);
return true;
}
bool chybaOtevreni(FILE *fp, Osoby &osoby) {
fclose(fp);
osoby.pocet = 0;
return false;
}
Obě funkce včetně jejich zařazení do menu najdete v sekci Download.
Závěr
To je pro dnešek vše. Pokud vám tento výklad nestačil, doporučuji, abyste si
přečetli nápovědu k uvedeným funkcím, je tam spousta zajímavých skutečností a
souvislostí. Děkuji za pozornost a těším se na další díl.
|