Čtení ze souboru

Pro čtení ze souboru můžeme použít funkci fread, která je protikladem funkce fwrite:

size_t fread(
    void* buffer,   // adresa, na kterou zapíšeme data ze souboru
    size_t size,    // velikost prvku, který načítáme
    size_t count,   // počet prvků, které načítáme
    FILE* stream    // soubor, ze kterého čteme
);

Tato funkce opět předpokládá, že budeme ze souboru načítat několik hodnot stejného datového typu. Například načtení pěti celých čísel, které jsme zapsali v kódu zde, by mohlo vypadat následovně:

#include <stdio.h>
#include <assert.h>

int main() {
    int pole[5] = { 1, 2, 3, 4, 5 };

    // otevření souboru
    FILE* soubor = fopen("soubor", "rb");
    assert(soubor);

    // čtení ze souboru
    int precteno = fread(pole, sizeof(int), 5, soubor);
    assert(precteno == 5);

    // zavření souboru
    fclose(soubor);    

    return 0;
}

Funkce fread vrací počet prvků, které úspěšně načetla ze souboru.

Textové čtení

Pokud bychom chtěli načítat ze souboru ASCII text, můžeme použít již známé funkce pro načítání textu, například fgets1 nebo fscanf, což je varianta funkce scanf určená pro formátované čtení ze souborů.

1S funkcí fgets jsme se setkali již dříve, kdy jsme jí jako poslední parametr globální proměnnou stdin. Datový typ proměnné stdin je právě FILE* – při spuštění programu standardní knihovna C vytvoří proměnné stdin, stdout a stderr a uloží do nich standardní vstup, výstup a chybový výstup.

U načítání dat si vždy dejte pozor na to, abyste na adrese, kterou předáváte do fread nebo fgets, měli dostatek naalokované validní paměti. Jinak by se mohlo stát, že data ze souboru přepíšou adresy v paměti, kde leží nějaké nesouvisející hodnoty, což by vedlo k paměťové chybě 💣.

Rozpoznání konce souboru

Při čtení ze souboru je třeba vyřešit jednu dodatečnou věc – jak rozpoznáme, že už jsme soubor přečetli celý a už v něm nic dalšího nezbývá? Pokud načítáme data ze souboru "binárně", tj. interpretujeme je jako byty a ne jako (ASCII) text, obvykle stačí si velikost souboru předpočítat po jeho otevření pomocí funkcí ftell a fseek nebo si ji přečíst přímo ze samotného souboru2.

2Spousta binárních formátů (např. JPEG) jsou tzv. samo-popisné (self-describing), což znamená, že typicky na začátku souboru je v pevně stanoveném formátu (tzv. hlavičce) uvedeno, jak je daný soubor velký. Využijeme toho například při práci s obrázkovým formátem TGA.

Co ale dělat, když načítáme textové soubory, jejichž formát obvykle není ani zdaleka pevně daný? Předpočítat si velikost souboru a pak muset po každém načtení např. řádku počítat, kolik znaků jsme vlastně načetli, by bylo relativně komplikované. Při čtení textových souborů se tak obvykle využívá jiná strategie – čteme ze souboru tak dlouho, dokud nedojde k chybě. Způsob detekce chyby záleží na použité funkci:

  • fscanf vrátí číslo <= 0, pokud se jí nepodaří načíst žádný zástupný znak ze vstupu.
  • fgets vrátí ukazatel s hodnotou 0, pokud dojde k chybě při čtení.

Jakmile dojde k chybě, tak bychom ještě měli ověřit, jestli jsme opravdu na konci souboru, anebo byla chyba způsobena něčím jiným3. To můžeme zjistit pomocí funkcí feof, která vrátí nenulovou hodnotu, pokud jsme se před jejím zavoláním pokusili o čtení a pozice již byla na konci souboru, a ferror, která vrátí nenulovou hodnotu, pokud došlo k nějaké jiné chybě při práci se souborem.

3Například pokud čteme soubor z USB flashky, který je během čtení odpojen od počítače.

Program, který by načítal a rovnou vypisoval řádky textu ze vstupního souboru, dokud nedojde na jeho konec, by tedy mohl vypadat například takto:

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main() {
    FILE* soubor = fopen("soubor.txt", "rt");
    assert(soubor);

    char radek[80];
    while (1) {
        if (fgets(radek, sizeof(radek), soubor)) {
            // radek byl uspesne nacten
            printf("Nacteny radek: %s", radek);
        }
        else {
            if (feof(soubor)) {
                printf("Dosli jsme na konec souboru\n");
            } else if (ferror(soubor)) {
                printf("Pri cteni ze souboru doslo k chybe: %s\n", strerror(errno));
            }

            break;
        }
    }

    fclose(soubor);

    return 0;
}

Kvíz 🤔

  1. Co vypíše následující program za předpokladu, že v souboru soubor.txt je tento obsah?

    radek1
    radek2
    radek3
    
    #include <stdio.h>
    #include <assert.h>
    
    int main() {
        FILE* soubor = fopen("soubor.txt", "r");
        assert(soubor);
    
        char radek[80];
        while (feof(soubor) == false) {
            fgets(radek, sizeof(radek), soubor);
            printf("Nacteny radek: %s", radek);
        }
    
        fclose(soubor);
        return 0;
    }
    
    Odpověď

    Program vypíše:

    radek1
    radek2
    radek3
    radek3
    

    Funkce feof vrátí pravdivou hodnotu pouze tehdy, kdy před jejím zavoláním na daném souborovém deskriptoru došlo k pokusu o čtení, který selhal z důvodu konce vstupního souboru. Po načtení prvních tří řádků tedy feof vrátí false, protože poslední pokus o čtení uspěl. Až v momentě, kdy se pokusíme načíst čtvrtý řádek, tak funkce fgets selže a poté feof vrátí true. Jelikož ale tento kód nekontroluje návratovou hodnotu funkce fgets a vždy po pokusu o načtení řádku vypíše proměnnou radek, tak se poslední řádek souboru vypíše dvakrát.