10. týždeň

Files

Súbory, argumenty príkazového riadku

Video (12'2022)

Prezentácia

Zdrojový kód

Poznámky k prednáške

The Files

  • slide Dnes sa budeme rozprávať o súboroch a o práci s nimi v jazyku C.

What is File?

  • (slide) Ešte predtým sa ale pokúsme zhrnúť znalosti, ktoré o súboroch máme. Pokúsme sa teda odpovedať na otázku, ktorá znie: "Čo je to súbor?"

  • (slide) Jedna z odpovedí môže byť:

    In C programming, file is a place on your physical disk where information is stored. (www.programiz.com)

Why files are needed?

  • (slide) Už teda vieme, čo to súbory sú. Ale prečo sa o nich máme vlastne rozprávať? Na čo sú dobré? Prečo sa tejto téme máme vôbec venovať?
  • Obecne sa dá povedať, že schopnosť programátora pracovať so súbormi patrí medzi jeho základné schopnosti a znalosti.
  • Poďme sa pozrieť na niekoľko dôvodov, pre ktoré sa oplatí vedieť pracovať so súbormi:
    • Keď sa program ukončí, sú automaticky stratené všetky údaje, ktoré mal v pamäti. Ak by sme však tieto údaje uložili pred ukončením programu do súboru, zabránime ich strate.
    • Ak váš program pracuje s obrovským množstvom údajov, ktoré je potrebné predtým ešte aj do programu zadať, určite vám život uľahčí to, ak si ich program načíta zo súboru miesto toho, aby ste ich zadávali ručne. Je to jednoduchšie, pohodlnejšie, vyhnete sa chybám pri zadávaní, a ak nie, tak ich viete jednoduchšie opraviť.
    • Súbory zjednodušujú prenos údajov medzi počítačmi bez zmien.

Types of Files

  • (slide) Existuje mnoho typov súborov v závislosti od toho, na čo ich budeme používať. Napríklad máme súbory, ktoré uchovávajú skladby, obrázky, filmy, iné zasa textové dokumenty alebo zdrojové súbory. Osobitnou kategóriou sú spustiteľné súbory.

  • (slide) Typy súborov sú štandardizované pomocou tzv. MIME typu. Jeho obecná forma je:

    type/subtype

    Opýtať sa na MIME typ súboru je možné pomocou nástroja file z príkazového riadku:

    $ file --mime-type cat.c
    cat.c: text/x-c
  • (slide) My však budeme kategorizovať súbory podstatne podstatne obecnejšie. Súbory budeme rozdeľovať na:

    • textové, a
    • binárne.
  • Textové súbory sú súbory, ktoré vieme vytvoriť bežnými textovými editormi. Rovnako tak ich vieme bez problémov čítať aj do nich zapisovať. Ich obsah je tvorený zobraziteľnými znakmi spolu s bielymi znakmi ASCII tabuľky.

  • Ich obsah vieme zobraziť aj priamo z príkazového riadku napríklad pomocou príkazu cat, napríklad:

    $ echo "hello world" > hello.txt
    $ cat hello.txt
    hello world
  • Naopak, obsah binárnych súborov je nám nečitateľný a na prácu s nimi potrebujeme špecifický softvér. Napríklad ak sa jedná o súbor s obrázkami, potrebujeme prehliadač obrázkov alebo grafický rastrový editor. Ak sa jedná o hudobný súbor, potrebujeme hudobný prehrávač, ktorý rozumie formátu súboru.

  • Pozor na to, že binárnymi súbormi sú aj súbory, s ktorými pracujú textové procesory!

  • Tento typ súborov obsahuje nie len zobraziteľné znaky z ASCII tabuľky, ale aj nezobraziteľné. Preto pri ich prezeraní vidíme aj niektoré texty, ale zvyšnému obsahu nerozumieme.

  • Na prácu s oboma typmi súborov sa dnes pozrieme podrobnejšie.

Print the Content of File

  • Prácu so súbormi začneme jednoduchým programom, ktorý bude pracovať ako príkaz cat - zobrazí, resp. vypíše obsah súboru na obrazovku. Na tomto príklade sa zoznámime so základmi práce so súborom.

Opening the File

  • Názov súboru, ktorého obsah máme zobraziť, načítame ako parameter nášho programu:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc != 2){
      fprintf(stderr, "Wrong number of parameters.\n");
      exit(EXIT_FAILURE);
    }
}
  • (slide) Následne tento súbor otvoríme pomocou funkcie fopen() , ktorá má dva parametre:

    1. názov súboru, a
    2. režim prístupu k súboru.
FILE *fp = fopen(argv[1], "r");
  • Názov súboru máme v premennej argv[1] a režim práce so súborom je r, čo znamená, že je určený iba na čítanie.
  • (slide) Prehľad existujúich režimov prístupu k súboru sa nachádza v nasledujúcej tabuľke:
File access mode Meaning Explanation File already exists File does not exist
"r" read open file for reading read from start failure to open
"w" write create file for writing destroy contents create new
"a" append append to file write to end create new
"r+" read extended open file for read/write read from start error
"w+" write extended create file for read/write destroy contents create new
"a+" append extended open file for read/write write to end create new

Poznámka

Ak budete programovať v inom jazyku, ako je C, je veľmi pravdepodobné, že sa s podobnými režimami, ako aj ich označením, stretnete aj tam.

  • (slide) Funkcia vráti referenciu typu FILE*, ktorá reprezentuje súbor. Tento údajový typ je zadefinovaný v súbore stdio.h.

Missing File

  • Čo sa však stane, ak spustíme program s názvom súboru, resp. umiestnením súboru, ktorý neexistuje?

  • Vykonávanie programu skončí s hláškou: Segmentation fault. To je kvôli tomu, že funkcia fopen() vráti hodnotu NULL vtedy, ak z nejakého dôvodu nebolo možné súbor otvoriť (napr. súbor neexistuje, zadaná cesta neexistuje, používateľ nemá práva na prístup k súboru a podobne). Aby sme sa vyhli príliš skorému ukončeniu programu, pripravíme sa aj na tento problém:

    if(fp == NULL){
        printf("Error opening file.\n");
        exit(EXIT_FAILURE);
    }

File Processing

  • (slide) Úlohou je vypísať obsah súboru na obrazovku. Prečítať znak zo súboru je možné volaním napríklad funkcie fgetc(), ktorá má jeden parameter a ním je referencia na otvorený súbor.
  • Takže jeden znak prečítame zo súboru takto:
    char ch = fgetc(fp);
    putchar(ch);
  • Ak by som chcel prečítať všetky znaky, tak by som mohol napríklad mohol "nakopypastovať" toľko čítaní a výpisov, koľko znakov má daný súbor. Alebo si to zjednodušiť nejakým cyklom. To však samozrejme nie je ideálne riešenie, pretože nie je univerzálne.
  • Takže máme problém - dokedy budeme čítať znaky zo súboru?
  • Začnime experimentálne s nekonečnou slučkou, aby sme videli, čo sa môže stať:
    while(1){
        char ch = fgetc(fp);
        putchar(ch);
    }
  • (slide) Keď program preložíme a spustíme, obrazovka sa zaplní jedným stále sa opakujúcim nezobraziteľným znakom. Ak totiž funkcia fgetc() dorazí na koniec súboru, tak vráti špeciálnu hodnotu, ktorá označuje koniec súboru. Touto hodnotou je -1 a definuje makro EOF (End of File). Toto makro je definované v súbore stdio.h.
  • Využijeme teda makro EOF na to, aby sme čítanie zo súboru včas ukončili:
    char ch = fgetc(fp);
    while(ch != EOF){
        putchar(ch);
        ch = fgetc(fp);
    }

Closing the File

  • Program síce spustíme a dokonca aj funguje ako má, ale dopúšťame sa stále jednej skrytej chyby. Tou chybou je, že súbor, ktorý máme otvorený, sme nezavreli.
  • Súbor sa uzavrie automaticky po skončení programu, pretože to za nás spraví operačný systém. Nie je však dobrou praxou sa na túto skutočnosť spoliehať, pretože počet otvorených súborov je obmedzený. Preto je dobré po skončení používania súboru ho rovno zatvoriť pomocou funkcie fclose().
  • Po skončení cyklu teda pridáme do nášho kódu riadok:
    fclose(fp);
  • Celé riešenie úlohy: cat.c.

The Problem

  • Môžeme vyskúšať, koľko súborov dokáže operačný systém otvoriť pre jeden proces. Pokúsime sa teda otvoriť 10000 súborov a uvidíme, čo sa stane (limit.c):
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){

    char filename[30];
    FILE* fp;

    for(int i = 1; i < 10000; i++){
        sprintf(filename, "file-%05d.txt", i);
        fp = fopen(filename, "w");
        fprintf(fp, "file with index: %d", i);
    }

    fclose(fp);
}
  • V programe sa nachádza dokonca aj volanie funkcie fclose(). V tom stave, v akom je použité však nebude zatvárať každý otvorený súbor, ale iba ten posledný.

  • Keď program spustíme, skončí s hláškou Segmentation fault. Ak si vylistujeme súbory v aktuálnom priečinku, tak zistíme, že sa ich podarilo vytvoriť okolo 1021.

  • Maximálny počet otvorených súborov pre jeden proces zistíme pomocou príkazu ulimit:

    $ ulimit -n
    1024
  • To teda znamená, že jeden proces môže mať naraz otvorených 1024 súborov. Ak vytváranie súborov skončilo pri čísle 1021 znamená to, že nám chýbajú ešte 3 súbory. Netreba zabudnúť pri rátaní samotný súbor programu a knižnice, ktoré program používa.

  • Aby sme sa vyhli tomuto problému, je dobré držať sa rady, že pre každý fopen() existuje jeden fclose(). Preto akonáhle napíšete funkciu fopen(), napíšte rovno aj funkciu fclose(). To aby ste nezabudli ;)

Simple head Implementation

  • V unix/linuxových operačných systémoch sa nachádza nástroj head, ktorý dokáže zobraziť časť súboru z jeho začiatku. Ak ho spustíme bez akýchkoľvek parametrov, tak jeho predvolené správanie je, že zobrazí prvých 10 riadkov súboru. Ak ho však spustíme s prepínačom -c 10, zobrazí prvých 10 znakov (bytov) súboru.
  • Skúsme urobiť jednoduchú implementáciu tohto nástroja v jazyku C, ktorá vypíše prvých 10 znakov súboru.
  • Ako základ implementácie použijeme predchádzajúci program cat.c, ktorý upravíme nasledovne (head.c):
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc != 2){
        fprintf(stderr, "Wrong number of parameters.\n");
        exit(EXIT_FAILURE);
    }

    FILE* fp = fopen(argv[1], "r");

    if(fp == NULL){
        fprintf(stderr, "Error opening file.\n");
        exit(EXIT_FAILURE);
    }

    for(int counter = 0; counter < 10; counter++){
        char ch = fgetc(fp);
        if(ch == EOF){
            break;
        }
        putchar(ch);
    }

    fclose(fp);
}

Simple tail Implementation

  • V unix/linuxových systémoch však existuje aj nástroj tail, ktorý dokáže zobraziť časť súboru z jeho konca. Podobne ako head bez akýchkoľvek parametrov vypíše posledných 10 riadkov súboru. Ak ho však spustíme s parametrom -c 10, zobrazí posledných 10 znakov (bytov) súboru.
  • Ak by sme chceli takúto implementáciu vytvoriť v jazyku C, ako budeme postupovať?
  • Nápady:
    • Ak by sme použili režim otvorenia súboru a (append), síce by sme sa veľmi rýchlo dostali na koniec súboru, ale nevieme sa dostať späť (aspoň zatiaľ).
    • Načítať obsah celého súboru do jedného obrovského jednorozmerného poľa znakov. Nevieme však dopredu povedať, aké veľké toto pole musí byť :-/
    • Využiť rekurziu. To by síce šlo a fungovalo celkom dobre pri malých súboroch. Réžia tohto riešenia by však rástla so zvyšujúcou sa veľkosťou súboru.

Function fseek()

  • (slide) Na riešenie tohto problému použijeme funkciu s názvom fseek(). Pomocou tejto funkcie sa je možné pohybovať v súbore smerom dopredu aj dozadu.
  • Funkcia fseek() má tri parametre:
    • stream - referencia na otvorený súbor
    • offset - vzdialenosť vľavo alebo vpravo od umiestnenia daného parametrom whence
    • whence - pozícia, ktorá môže mať tri hodnoty: SEEK_SET, SEEK_CUR alebo SEEK_END

fseek() Usage Illustrations

  • Keď súbor otvoríme v textovom editore, tak pozíciu kurzora vieme identifikovať pomocou riadka a stĺpca. Obsah súboru je však na diskú usporiadaný lineárne. To znamená, že neexistujú riadky a stĺpce, ale jeho aktuálna pozícia je definovaná ako vzdialenosť (offset) od začiatku súboru.
  • V súbore vieme jednodznačne identifikovať tri miesta:
    • začiatok súboru, ktorý pre použitie funkcie fseek() označujeme ako SEEK_SET,
    • koniec súboru, ktorý pre použitie funkcie fseek() označujeme ako SEEK_END, a
    • aktuálnu polohu v súbore, ktorý pre použitie funkcie fseek() označujeme ako SEEK_CUR.
  • Takže teraz už môžeme bez väčších problémov napísať jednoduchú implementáciu nástroja tail (tail.c):
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc != 2){
        fprintf(stderr, "Wrong number of parameters.\n");
        exit(EXIT_FAILURE);
    }

    FILE* fp = fopen(argv[1], "r");
    if(fp == NULL){
        fprintf(stderr, "Error opening file.\n");
        exit(EXIT_FAILURE);
    }

    fseek(fp, -11, SEEK_END);

    char ch = fgetc(fp);
    while(ch != EOF){
        putchar(ch);
        ch = fgetc(fp);
    }

    fclose(fp);
}

Poznámka

Pozor však na posledný znak na konci súboru!

Determining File Size

  • Na tomto mieste stojí za to sa zamyslieť aj nad tým, ako zistíme veľkosť súboru. Najjednoduchší spôsob by mohol byť ten, že prejdeme súbor znak po znaku a budeme počítať, koľko znakov sme prešli do konca súboru. To je síce riešenie, ale má príliš veľa vstupno/výstupných operácií (toľkokrát budeme čítať zo súboru, koľko znakov obsahuje).
  • (slide) Našťastie sa v jazyku C nachádza funkcia ftell(), ktorá vráti aktuálnu pozíciu kurzoru v súbore. To znamená, že vráti offset alebo vzdialenosť od začiatku súboru po aktuálnu polohu.
  • To sa nám dosť hodí pri zisťovaní veľkosti súboru. Teraz už stačí len:
    • presunúť sa na koniec súboru pomocou funkcie fseek(), a
    • zistiť vzdialenosť od jeho začiatku pomocou funkcie ftell().
  • Výsledný kód bude vyzerať takto (size.c):

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc != 2){
        fprintf(stderr, "Wrong number of parameters.\n");
        exit(EXIT_FAILURE);
    }

    FILE* fp = fopen(argv[1], "r");
    if(fp == NULL){
        fprintf(stderr, "Error opening file.\n");
        exit(EXIT_FAILURE);
    }

    fseek(fp, 0, SEEK_END);
    int size = ftell(fp);

    printf("size is: %d\n", size);

    fclose(fp);
}

Upozornenie

Pozor na to, ak je súbor o 1B väčší ako obsah, ktorý sa v ňom nachádza. Pri bližšom pohľade na tento znak cez niektorý z hexa editorov (napríklad mcedit) prídete na to, že sa jedná o znak 0x0A - nový riadok. Mylne sa môžete domnievať, že sa vlastné jedná o EOF. To ale nie je pravda - EOF nie je súčasťou súboru! Je to len dohodnutá hodnota, ktorú vracajú funkcie pracujúce so súbormi.

Posledný znak v súbore 0x0A do neho pridávajú automaticky textové editory. Je to kvôli tomu, ako riadok definuje POSIX:

  • 3.206 Line
  • A sequence of zero or more non- <newline> characters plus a terminating <newline> character. Toto správanie nájdete aj v mnohých editoroch, ako je napr. vim, gedit, ... Dá sa však vypnúť. Napr. v editore vim to urobíte takto:
:set binary
:set noeol

File Copy

  • Ako jeden z dôvodov, pre ktorý sa oplatí vedieť so súbormi pracovať, sme v úvode spomínali, že súbory je možné používať na prenos dát medzi zariadeniami bez straty. Na to, aby sme súbory mohli medzi zariadeniami prenášať, potrebujeme vytvoriť ich kópiu. Vyskúšame teda spraviť jednoduchú implementáciu príkazu cp na kopírovanie súborov.
  • Vytvoríme si "kombinovaný" program pre čítanie aj zápis zároveň. Z jedného súboru budeme údaje čítať a do druhého ich budeme zapisovať. Oba tieto súbory zadáme ako parametre príkazového riadku.
  • Ako základ pre našu implementáciu vieme použiť program cat.c na zobrazenie obsahu súboru. Aj pri kopírovaní potrebujeme načítať každý jeden znak, akurát ho nebudeme vypisovať, ale zapisovať do súboru.
  • Základná verzia súboru cp.c teda môže vyzerať nasledovne:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc != 3){
        fprintf(stderr, "Wrong number of parameters.\n");
        exit(EXIT_FAILURE);
    }

    FILE* fps = fopen(argv[1], "r");
    if(fps == NULL){
        printf("Error: The file '%s' could not be opened.\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    FILE* fpd = fopen(argv[2], "w");
    if(fpd == NULL){
        printf("Error: The file '%s' could not be opened.\n", argv[2]);
        exit(EXIT_FAILURE);
    }

    char ch = fgetc(fps);
    while(ch != EOF){
        fputc(ch, fpd);
        ch = fgetc(fps);
    }

    fclose(fps);
    fclose(fpd);
}
  • Ak program spustíme, prekopíruje zdrojový súbor do cieľového a skončí. V prípade, že sa zdrojový alebo cieľový súbor nepodarí otvoriť, dôjde ku chybe. Rovnako dôjde ku chybe aj vtedy, keď sa program spustí s nesprávnym počtom parametrov.

  • To, či súbor bol prekopírovaný bez straty, vieme overiť príkazom diff nasledovne:

    $ diff source_file destination_file
  • V prípade, že sú súbory rovnaké, na výstupe sa nič nezobrazí. Ak sa však zdrojový súbor oproti cieľovému líši, zobrazí na výstupe riadky, v ktorých je rozdiel.

Number of I/O Operations

  • Skúsme zistiť, koľko vstupno/výstupných (V/V) operácií sme urobili na to, aby sme súbor prekopírovali zo zdroja do cieľa.

  • Za vstupnú operáciu budeme považovať volanie akejkoľvek funkcie, ktoré číta údaje zo súboru. Za výstupnú operáciu budeme zasa považovať volanie akejkoľvek funkcie, ktorá vie údaje do súboru zapísať. Počet vykonaných V/V operácií získame ako súčet vstupných a výstupných operácií.

  • Upravíme náš program tak, že do neho pridáme dve počítadlá:

    1. input_counter - počítadlo vstupných operácií, a
    2. output_counter - počítadlo výstupných operácií.
  • Modifikujeme teda fragment kódu zabezpečujúci kopírovanie súboru nasledovne:

    int input_counter = 0;
    int output_counter = 0;

    char ch = fgetc(fps);
    input_counter++;

    while(ch != EOF){
        fputc(ch, fpd);
        output_counter++;
        ch = fgetc(fps);
        input_counter++;
    }
  • Nakoniec už len vypíšeme počet V/V operácií na obrazovku (cp1.c):
    printf("There were %d/%d I/O operations.\n", input_counter, output_counter);
  • Keď program vykomplikujeme a spustíme, na výstupe uvidíme dve čísla - prvé hovorí o počte vstupných operácií a druhé hovorí o počte výstupných operácií. Počet vstupných je 1 vyšší ako počet výstupných operácií. To je kvôli tomu, že je potrebé vykonať o jednu vstupnú operáciu naviac kvôli tomu, aby sme zistili, že sa jedná o koniec súboru. Každopádne počet zápisov je totožný s počtom znakov vo vstupnom súbore.
  • Ale - kopírovať súbor znak po znaku? To je príliš drahá operácia. To je ako chodiť do drevárne po drevo a zakaždým z nej odniesť len jedno polienko - bude to dlho trvať a vy sa riadne spotíte. Rovnako je to aj v počítači - počítač sa riadne pri takejto operácii zahreje.
  • Akým spôsobom by sa teda dala táto operácia zrýchliť? Ak sa vrátime ku ilustrácii s nosením dreva, tak tento problém vieme vyriešiť napríklad použitím fúrika alebo košíka alebo opálky, do ktorých vieme naraz naložiť viac polienok ako len jedno. Ako by sme vedeli takýto "fúrik" použiť v jazyku C?
  • (slide) Tento problém môžeme vyriešiť tak, že budeme textový súbor kopírovať po slovách. Použijeme na to funkcie fscanf() a fprintf(), ktoré pracujú podobne ako v prípade štandardného vstupu. Pripravíme si teda "fúrik", ktorý v programovaní nazývame buffer a budeme ho používať na prenos, resp. kopírovanie slov zo vstupného do výstupného súboru.
  • Vo všeobecnosti teda pripravíme buffer o istej veľkosti, ktorý naplníme slovom zo vstupného súboru a toto slovo zapíšeme do výstupného súboru. Podobne ako chodíme do drevárne s košíkom, ktorý naplníme polienkami (cp2.c):
    int input_counter = 0;
    int output_counter = 0;
    char buffer[10];

    while(fscanf(fps, "%s", buffer) != EOF){
        input_counter++;
        fprintf(fpd, "%s ", buffer);
        output_counter++;
    }

    printf("There were %d/%d I/O operations.\n", input_counter, output_counter);
  • Po vykomplikovaní a spustení sa počet operácií rapídne znížil. Ak sa však pokúsime porovnať oba súbory pomocou príkazu diff, prídeme na to, že súbory nie sú vôbec zhodné. O tom sa môžeme presvedčiť aj zobrazením a provnaním veľkostí oboch súborov. Čo sa vlastne stalo?
  • Funkcia fscanf() podobne ako funkcia scanf() číta reťazce, ktoré sa nachádzajú medzi bielymi znakmi , pričom biele znaky ako také ignoruje. Takže obsah súboru zrejme zostal nezmenený, ale stratili sme biele znaky nachádzajúce sa medzi slovami.
  • Súbor sme teda síce prekopírovali, ale so stratami.
  • Skúsme teda použiť funkciu fgets(), s ktorou sme sa už stretli na predchádzajúcich prednáškach. Iste si pamätáte, že táto funkcia dokáže prečítať celý riadok. Doposiaľ sme pracovali s stdin, teda štandardným vstupom. Tentokrát namiesto neho jednoducho použijeme referenciu na súbor (cp3.c):
    int i_counter = 0, o_counter = 0;
    char buffer[300];

    while (fgets (buffer, 300, fps) != NULL){
        i_counter++;
        fputs (buffer, fpd);
        o_counter++;
    }

    printf ("There were %d/%d I/O operations.\n", i_counter, o_counter);

Buffer Overflow

  • (slide) Je tu však ešte jeden problém. Ak totiž buffer, do ktorého sa riadky načítavajú, bude príliš malý, môže dôjsť k pretečeniu. Funkcia fscanf() podobne ako aj funkcia scanf() nekontroluje, či buffer, do ktorého má uložiť načítané údaje, je dostatočne veľký. To sa môže stať veľmi jednoducho práve pri práci s reťazcami. V tom lepšom prípade sa program ukončí s hláškou Segmentation fault, v tom horšom prepíše časť pamäte a bude pokračovať.
  • Podobne pri funkcii fgets() sa môže stať, že riadok bude príliš dlhý a do zásobníka sa nezmestí, a opäť budeme kopírovať so stratami.

Functions fread() and fwrite()

  • Zámer bol síce dobrý, ale na jeho realizáciu sme použili nesprávne funkcie. Potrebovali by sme také funkcie, pomocou ktorých naplníme buffer doplna nehľadiac na to, o aké znaky sa jedná.

  • (slide) Také funkcie samozrejme existujú. Pre načítavanie údajov budeme používať funkciu fread() a pre zápis budeme používať funkciu fwrite().

  • Obe funkcie majú rovnaký počet ako aj význam parametrov. K tomu, aby sme porozumeli ich významu ako aj správaniu oboch funkcií, nám pomôže nasledovná ilustrácia.

  • Pred chvíľou sme hovorili o tom, že keď chceme z drevárne priniesť drevo, použijeme na to fúrik, košík alebo opálku. Proste niečo, pomocou čoho budeme schopní odniesť naraz viac polienok ako len jedno.

  • Nie vždy je to však len o počte. Predstavte si, že máte naložiť alebo vyložiť kamión plný piva. Vieme, že na prenos sklenených fliaš sa bežne používajú prepravky. Každá prepravka je charakteristická tým, koľko flašiek sa do nej vojde. Napríklad do prepravky na pivné fľaše IML je možné vložiť 20 fliaš piva o objeme 0.5l.

  • Ak by sme ale chceli prepraviť víno vo fľašiach, nemôžeme na to použiť prepravky na pivo. Víno je totiž možné kúpiť vo fľašiach ako rozličného tvaru, tak aj rozličného objemu. Ak pominieme rozličný tvar, vieme hovoriť o objemoch vína 0.7l, 1l alebo 2l. A na každú takúto fľašu potrebujeme iný typ prepravky.

  • Podobne to bude aj s použitím funkcií fread() a fwrite() - okrem samotnej prepravky (buffer) budeme potrebovať vedieť aj to, koľko fľašiek do nej vojde (number of members) a aká veľká je jedna fľaša (size). Parametre týchto funkcií sú:

    • void *ptr - referencia na buffer,
    • size_t size - veľkosť jedného prvku (v pamäti), na čo použijeme operátor sizeof(),
    • size_t nmemb - počet prvkov (v buffri), a
    • FILE *stream - otvorený súbor.
  • Obe funkcie vracajú počet úspešne prečítaných alebo uložených prvkov. Tento počet je samozrejme v rozsahu <0, nmemb>. To teda znamená, že celý čas sa budú súbory zapisovať a čítať v plných buffroch, ale poslednýkrát do plného buffra niečo chýbať.

  • Pokúsime sa teda implementovať nástroj cp s použitím týchto dvoch funkcií (cp4.c):

    int input_counter = 0;
    int output_counter = 0;
    char prepravka[20];

    int count = fread(prepravka, sizeof(char), 20, fps);
    input_counter++;
    while(count > 0){
        fwrite(prepravka, sizeof(char), count, fpd);
        output_counter++;
        count = fread(prepravka, sizeof(char), 20, fps);
        input_counter++;
    }

    printf("Number of I/O operations: %d/%d\n", input_counter, output_counter);
}
  • Tentokrát po porovnaní zdrojového a cieľového súboru pomocou nástroja diff nedôjde ku žiadnemu rozdielu, a teda súbory budú naozaj totožné.

Binary Files

  • (slide) Nakoniec nám už zostáva sa pozrieť len na binárne súbory. Ako sme hovorili na začiatku - jedná sa o súbory, ktorých obsah je tvorený všetkými hodnotami, ktoré je možné zapísať na 8b.
  • Aká je však výhoda použitia binárnych súborov oproti textovým? Obecne je možné povedať, že používanie binárnych súborov oproti textovým má tú výhodu, že čítanie aj zápis údajov je rýchlejšie. To je dané tým, že dochádza priamo k zápisu údajov z pamäti do súboru a opačne. V prípade textových súborov musia byť všetky údaje prevedené na text a až potom môžu byť zapísané. To platí aj opačne - predtým, ako budú údaje uložené do pamäte, musia byť prevedené z textu na cieľový údajový typ. A to operácie čítania a zápisu značne zdržiava.
  • Na prácu s binárnymi súbormi budeme používať funkcie fread() a fwrite(), pretože bude záležať na každom jednom byte.

Writing Binary Files

  • Začnime teda tým, že vyskúšame zapísať do súboru číslo a porovnáme veľkosť zapísaných údajov v prípade textového a binárneho zápisu.
  • Ak napríklad budeme chcieť zapísať hodnotu 12345 do textového súboru, bude tento súbor mať dĺžku 5B, pretože na zapísanie hodnoty 12345 potrebujeme 5 číslic. Akú dĺžku však bude mať tento údaj zapísaný do binárneho súboru?
  • Veľkosť súboru bude priamo úmerná údajovému typu, ktorý použijeme na uchovanie tejto hodnoty:
Type Size
int 4B
long 8B
short int 2B
  • Na ilustráciu teda použijeme nasledujúci fragment kódu, pomocou ktorého zapíšeme hodnotu 12345 ako hodnotu typu integer do súboru. Súbor tentokrát otvoríme v režime wb - na zápis v binárnom režime (binary-write.c):
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    FILE* fp = fopen("data.bin", "wb");
    if(fp == NULL){
        fprintf(stderr, "Error: The file 'data.bin' could not be opened.\n");
        exit(EXIT_FAILURE);
    }

    int value = 12345;
    fwrite(&value, sizeof(int), 1, fp);
    fclose(fp);
}
  • Po prekompilovaní a spustení vznikne súbor s názvom data.bin, ktorého veľkosť sú 4B. Ak by sme miesto údajového typu použili typ short int, budú to len 2B.
  • Veľkosť textového súboru záleží od množstva zapísaných číslic. Jeho veľkosť teda môže byť od 1B, ak zapisujeme len jednocifernú hodnotu, až po nB, kde n je počet číslic potrebných na zapísanie maximálnej hodnoty daného údajového typu.
  • Veľkosť binárneho súboru bude však vždy konštantná, pretože v pamäti bude stále zaberať rovnaké miesto nezávisle od jeho hodnoty.

Poznámka

Ak samozrejme budeme zapisovať do súboru reťazce, ich dĺžka bude rovnaká, ako v prípade textových súborov. Tu však môže nastať problém pri ich spätnom čítaní, pretože nemusíme dopredu vedieť jeho dĺžku.

Reading Binary Files

  • Pri čítaní údajov z binárnych súborov musíme dávať pozor na:

    • poradie, v ktorom boli údaje zapísané (v rovnakom ich musíme čítať), a na
    • údajový typ použitý na zapísanie hodnoty (môže dôjsť k prečítaniu väčšieho alebo menšieho množstva údajov, ako bolo v skutočnosti zapísaných, a tým pádom môže byť načítaná hodnota iná, ako tá, ktorá bola uložená).
  • Ak budeme chcieť spätne načítať uloženú hodnotu, môžeme to urobiť takto (binary-read.c):

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    FILE* fp = fopen("data.bin", "rb");
    if(fp == NULL){
        fprintf(stderr, "Error: The file 'data.bin' could not be opened.\n");
        exit(EXIT_FAILURE);
    }

    int value = 0;
    fread(&value, sizeof(int), 1, fp);
    printf("The value is: %d\n", value);
    fclose(fp);
}
  • Po preložení a spustení programu sa na obrazovku objaví prečítaná hodnota. Čo však v prípade, ak je súbor prázdny? Výsledkom bude hodnota 0.
  • Aby sme tomuto problému predišli, overíme, či sme prečítali zo súboru toľko hodnôt, ako sme v skutočnosti prečítať chceli. Upravíme teda predchádzajúci program nasledovne:
    int value = 0;
    if(fread(&value, sizeof(int), 1, fp) != 1){
        printf("Error: The data were not read.\n");
        exit(EXIT_FAILURE);
    }
    printf("The value is: %d\n", value);
    fclose(fp);

Conclusion

  • Na tejto prednáške sme si ukázali základy práce s textovými a binárnymi súbormi.
  • Čaro práce s binárnymi súbormi objavíme však až v letnom semestri, keď sa budeme venovať štruktúram. Ukážeme si, ako jednoducho je možné do súboru ukladať a následne čítať celé štruktúry údajov. Procesu ukladania takýchto údajov budeme hovoriť serializácia a procesu načítavania údajov zo súboru budeme hovoriť deserializácia.
  • Vidíme sa v letnom semestri ;)

Doplňujúce zdroje

  1. C Programming Files I/O
  2. Buffer Overflow/Overrun
  3. man fseek - sets the file position indicator for the stream pointed to by stream c-reference cplusplus.com
  4. man ftell - obtains the current value of the file position indicator for the stream pointed to by stream c-reference cplusplus.com
  5. man fprintf - produce output according to a format to a given output stream (file) c-reference cplusplus.com
  6. man fscanf - scans input according to format from a given input stream (file) c-reference cplusplus.com
  7. man fread - binary stream input c-reference cplusplus.com
  8. man fwrite - binary stream output c-reference cplusplus.com
  9. MIME types - A Multipurpose Internet Mail Extensions (MIME) type is a standard that indicates the nature and format of a document, file, or assortment of bytes.
  10. The Basics of C Programming - Článok zo série o programovaní v jazyku C. Tentokrát na tému Binary Files.
  11. POSIX Definitions - definícia riadku podľa POSIX-u
  12. What is the difference between ASCII Chr(10) and Chr(13)