10. cvičení

Práce se soubory

Třídy pro práci se soubory jsou dostupné v hlavičkovém souboru fstream, který je potřeba includovat na začátku zdrojového souboru:

#include <fstream>

Čtení ze souboru

Pro práci se soubory, ze kterých chceme číst data, slouží třída std::ifstream (input file stream). Nejprve potřebujeme otevřít nějaký soubor, což můžeme provést buď pomocí metody open:

ifstream soubor;
soubor.open("data.txt");

Nebo přímo předáním názvu souboru do konstruktoru třídy ifstream:

ifstream soubor("data.txt");

Otevírání souboru je ale operace, která může selhat (např. když vstupní soubor neexistuje), takže bychom měli ověřit, jestli se operace povedla. To můžeme provést např. pomocí metody good, která vrací hodnotu true, pokud je daný objekt v pořádku, a false, pokud nastala nějaká chyba:

if (soubor.good()) {
    cout << "soubor otevren" << endl;
}
else {
    cout << "soubor neslo otevrit" << endl;
    return 1;
}

Dále můžeme přistoupit k samotnému čtení dat. Jelikož třída ifstream představuje „stream“, můžeme pro čtení dat použít operátor >>, např.:

string s;
soubor >> s;
cout << "1. slovo je " << s << endl;

Také můžeme použít funkci getline:

getline(soubor, s);
cout << "zbytek radku je " << s << endl;

Operátor >> funguje nejen pro textové řetězce, ale také např. pro čtení čísel:

int a = 0;
soubor >> a;
int b = 0;
soubor >> b;
cout << "cisla jsou " << a << " a " << b << endl;

Zde je ale potřeba dát pozor na to, že čtení se nemusí vždy povést, a proto bychom po každém pokusu o čtení dat měli zkontrolovat, jestli se to povedlo. Pokud bychom kontroly neprováděli, objekt soubor by zůstal ve stavu s chybou a žádné další čtení by se neprovedlo, takže program by bez kontrol nemusel pracovat správně. Pro kontrolu můžeme opět použít metodu good:

int c;
soubor >> c;
if (soubor.good()) {
    cout << "c = " << c << endl;
}
else {
    cout << "chyba" << endl;
}

Zápis do souboru

Pro práci se soubory, do kterých chceme zapisovat data, slouží třída std::ofstream (output file stream). Otevření výstupního souboru se provede podobně jako otevření vstupního souboru:

ofstream vystup("vystup.txt");

if (!vystup.good()) {
    cout << "vystupni soubor neslo otevrit" << endl;
}

Pro zápis dat do souboru slouží operátor <<:

vystup << "Hello, world!" << endl;

Výstupní soubor lze vlastně používat stejně jako objekt cout pro výpis do terminálu. Dokonce můžeme definovat funkce s parametrem typu ostream, za který můžeme dosadit jak objekty typu ofstream, tak i objekt cout:

void vypocet(ostream& out)
{
    out << "vystupni text" << endl;
}

Potom uvnitř nějaké jiné funkce můžeme použít buď vypocet(vystup); nebo vypocet(cout);, podle toho, kam chceme data vypsat.

Pozor na to, že parametr out musí být reference, tj. ostream& místo ostream. Důvod spočívá v tom, že pro objekty typu std::ifstream a std::ofstream nelze vytvářet kopie, takže je lze předávat pouze pomocí reference.

Příklad: funkce pro počítání slov v souboru

Pro počítání slov v souboru můžeme využít funkci z předchozího cvičení, která počítá slova v textovém řetězci. Soubor pak můžeme zpracovat po řádcích s využitím funkce getline a pro každý řádek použít již hotovou funkci pocet_slov.

int pocet_slov(string text)
{
    // funkce naprogramovana stejne jako na predchozim cviceni...
}

int pocet_slov(ifstream& soubor)
{
    int pocet = 0;

    while (soubor.good()) {
        string s;
        getline(soubor, s);
        pocet += pocet_slov(s);
    }

    return pocet;
}

Čtení souboru provádíme, dokud je objekt soubor v pořádku (jeho metoda good vrací true), čili dokud nenastane nějaká chyba nebo dokud nedojdeme na konec souboru.

Všimněte si také, že máme dvě funkce se stejným názvem. To ale ničemu nevadí, protože překladač jazyka C++ se (v některých případech) dokáže rozhodnout podle typu parametrů, kterou z definovaných funkcí má v daném případě použít.

Alternativně můžeme počítání slov v souboru naprogramovat pomocí operátoru >>, který se postará o rozdělení na jednotlivá slova:

int pocet_slov2(ifstream& soubor)
{
    int pocet = 0;

    while (soubor.good()) {
        string s;
        soubor >> s;
        pocet++;
    }

    return pocet;
}

Tyto funkce můžeme použít a porovnat např. takto:

int main()
{
    ifstream vstup;
    vstup.open("data.txt");
    if (!vstup.good()) {
        cout << "soubor neslo otevrit" << endl;
        return 1;
    }

    cout << "pocet slov v souboru je " << pocet_slov(vstup) << endl;

    // ekvivalentni podminka:  if (!vstup.good() && !vstup.eof()) ...
    if (vstup.fail() || vstup.bad()) {
        cout << "ale pri pocitani doslo k chybe" << endl;
        return 1;
    }

    // prejdeme zpet na zacatek souboru
    vstup.seekg(0);

    cout << "pocet slov v souboru je " << pocet_slov2(vstup) << endl;

    if (vstup.fail() || vstup.bad()) {
        cout << "ale pri pocitani doslo k chybe" << endl;
        return 1;
    }

    return 0;
}

Zde po každém použití funkce pocet_slov nebo pocet_slov2 navíc kontrolujeme, jestli při provádění funkce nedošlo k chybě čtení souboru (potom by výsledek nemusel odpovídat realitě). Mezi oběma funkcemi navíc používáme metodu seekg, která nás přesune zpět na začátek souboru, aby druhá funkce měla také co číst.