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.

Andrei Badea