C/C++ (24) - Soubory

Dnes probereme operace se soubory. Ukážeme si, jak naprogramovat zjednodušené vlastní verze příkazů cat a cp.

15.9.2005 06:00 | Jan Němec | přečteno 66417×

Soubory

Funkce pro základní operace se soubory nalezneme ve standardní knihovně jazyka C. Podobně jako v případě terminálového vstupu a výstupu musíme inkludovat stdio.h. C rozlišuje soubory textové a binární (obecné) a pro oba typy nabízí odlišnou sadu funkcí. Ve skutečnosti je z hlediska každého normálního operačního systému obsah běžného souboru jen N-tice bytů a to, zda se jedná o soubor textový nebo binární, je jen vlastností režimu práce s ním. V textovém režimu se díváme na jeho obsah jako na textová data, s tím, že některé znaky nebo jejich sekvence mají speciální význam, jde například o odřádkování, tabelátor nebo znak konce souboru. Konkrétní sada a interpretace těchto sekvencí pak závisí na operačním systému, proto například unixový textový soubor vypadá po otevření v notepadu ve Windows jako jedna dlouhá řádka s vloženými nezobrazitelnými znaky v místech původních konců řádek.

Soubor, který obsahuje pouze textová data, je zpravidla výhodnější zpracovávat v C v textovém režimu, ve výjimečných případech (například právě pokud chceme na Linuxu generovat soubor čitelný v notepadu) však použijeme i na textové soubory binární režim. Naproti tomu na soubor s jiným než textovým obsahem použít textový režim sice také můžeme, ale rozumný smysl to nemá.

Textový režim

Nejjednodušším smysluplným příkladem je výpis textového souboru na standardní výstup. Následující program je ekvivalentem příkazu

cat /etc/passwd
#include <stdio.h>

int main(void) {
  FILE *fr;
  char s[1024];
  
  fr = fopen("/etc/passwd", "r");
  if (!fr) {
    fputs("Nemohu otevřít vstupní soubor.\n", stderr);
    return 1;
  }
  while (fgets(s, sizeof(s), fr) != NULL) {
    fputs(s, stdout);
  }
  fclose(fr);
  return 0;
}

Pro soubory se ve standardní knihovně používají proměnné typu ukazatel na FILE, což je strukturovaný typ, ale jeho konkrétní položky nás nemusejí zajímat, slouží pro vnitřní potřebu runtime podpory jazyka C, programátor pracuje pouze s ukazatelem na celou strukturu. První operace je otevření souboru /etc/passwd. Funkce fopen vrací ukazatel na FILE - otevřený soubor, prvním parametrem je jeho jméno a druhým režim práce, v našem případě "r" znamená textový soubor otevřený pro čtení. Otevření souboru se nemusí podařit, proto je třeba otestovat návratovou hodnotu. Funkci fgets už známe z dílu o standardním vstupu, jedná se o čtení ze souboru fr řádky maximální délky sizeof(s) do bufferu s. Pokud je řádka delší, funkce přečte jen její začátek a případné další čtení ze souboru pomocí funkce fgets začne na místě, kde původní volání skončilo. I zde návratová hodnota NULL znamená neúspěch, například konec souboru. Funkce fputs je obdoba puts, pouze se píše do souboru. V našem případě je ovšem oním souborem stdout, tedy standardní výstup. Podobně můžeme používat i proměnné stdin a stderr se zřejmým významem. Rozdíl mezi puts(s) a fputs(s, stdout) je pouze v tom, že puts ještě navíc kromě výpisu řetězce odřádkuje, což se nám teď nehodí, neboť řetězec s již obsahuje znak nového řádku. Po ukončení práce se souborem je třeba jej zavřít funkcí fclose. Důležité je to především v případě zápisu, neboť teprve fclose vyprázdní buffery standardní C knihovny a fyzicky data uloží. Po čtení ze souboru voláme fclose "jen" z důvodu šetření systémových prostředků a programátorské slušnosti.

Funkcí pro práci se soubory v textovém režimu je více a většinou jim (názvem i chováním) odpovídá nějaké funkce pro práci se standardním vstupem a výstupem, kterou jsme si již popsali v počátečních dílech našeho seriálu.

FILE *fopen(const char *path, const char *mode);

Výjimkou je funkce fopen. V příkladu jsme si ukázali otevření /etc/passwd pro čtení příkazem

fopen("/etc/passwd", "r");

V případě zápisu do textového souboru použijeme jako druhý parametr "w" nebo "a". Pokud soubor neexistuje, v obou případech je vytvořen nový. Rozdíl nastane u existujícího souboru. Otevření pomocí "w" smaže původní obsah souboru (například textový editor ukládá soubor), zatímco "a" znamená zápis na konec souboru (například přidání nového uživatele do /etc/passwd). Funkce fopen nemusí vždy uspět, nejčastější příčinou je nedostatečné oprávnění uživatele k danému souboru nebo adresáři.

Pro zápis můžeme použít uvedené analogie funkcí puts, putchar a printf. Hlavním rozdílem je, že fputs sama od sebe neodřádkuje, jinak se všechny tři funkce chovají podobně jako jejich protějšky pro standardní výstup.

int fputs(const char *s, FILE *stream);
int fputc(int c, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);

Podobné je to i u čtení, i zde máme analogie funkcí gets, getchar a scanf. Funkce gets má navíc parametr size, který omezí maximální délku přečtené řádky (narozdíl od gets, která v případě nekonečné řádky čte tak dlouho, až program spadne na přetečení bufferu a nelze tomu nijak zabránit), a pokud narazí na konec řádky, uloží do bufferu i znak '\n', zatímco gets jej zahodí.

int fgets(char *s, int size, FILE *stream);
int fgetc(FILE *stream);
int fscanf(FILE *stream, const char *format, ...);

Po skončení čtení nebo zápisu soubor uzavřeme funkcí fclose.

int fclose(FILE *stream);

Binární režim

Pro práci se souborem, chápaným jako pole bytů, použijeme binární režim. Následující příklad zhruba odpovídá příkazu

cp /etc/passwd /home/honza/passwd

Všimněte si, že použijeme binární mód pro kopírování souboru obsahující textová data.

#include <stdio.h>

int main(void) {
  FILE *fr, *fw;
  unsigned char buf[1024];
  size_t precteno, zapsano;
  
  fr = fopen("/etc/passwd", "rb");
  if (!fr) {
    fputs("Nemohu otevřít vstupní soubor.\n", stderr);
    return 1;
  }
  fw = fopen("/home/honza/passwd", "wb");
  if (!fw) {
    fclose(fr); 
    fputs("Nemohu otevřít výstupní soubor.\n", stderr);
    return 2;
  }
  
  while (precteno = fread(buf, 1, sizeof(buf), fr)) {
    zapsano = fwrite(buf, 1, precteno, fw);
    if (precteno > zapsano) {
      fputs("Chyba zápisu do souboru.\n", stderr);
      break;
    }
  }
  if (ferror(fr)) {
    fputs("Chyba čtení ze souboru.\n", stderr);
  }
  fclose(fr);
  fclose(fw);
  return 0;
}

Nejprve jsme oba soubory otevřeli funkcí fopen. Písmeno 'b' v parametru určujícím režim práce se souborem, znamená binární mód. Vlastní kopírování probíhá ve while cyklu, který může skončit chybou čtení nebo zápisu a nebo koncem souboru /etc/passwd. Funkce fread čte ze souboru fr do bufferu buf sizeof(buf) bloků dat velikosti 1 byte. Návratovou hodnotou je počet přečtených bloků, který je v našem případě shodou okolností rovný počtu přečtených bytů. Pokud funkce narazí na konec souboru nebo dojde v průběhu čtení k chybě, vrátí funkce menší počet bloků, případně nulu. Zápis do /home/honza/passwd probíhá zcela analogicky funkcí fwrite. Zde je menší než požadovaný počet zapsaných bloků dat vždy chybou. Po ukončení kopírovacího cyklu je dobré se ještě ujistit, že cyklus neskončil kvůli chybě čtení ze souboru pomocí ferror, neboť z návratové hodnoty fread nepoznáme, zda jsme jen narazili na konec souboru nebo došlo k nějaké (například diskové) chybě.

Nyní si ukážeme nejdůležitější funkce pro binární režim.

FILE *fopen(const char *path, const char *mode);

Použití funkce fopen se od textového režimu liší především přidáním 'b' do řetězce mode, navíc má rozumný smysl otevřít soubor pro čtení i zápis zároveň.

modesoubor existovalsoubor neexistoval
rbotevře pro čteníchyba
rb+otevře pro čtení a zápischyba
wbsmaže obsah a otevře pro zápisvytvoří a otevře pro zápis
wb+smaže obsah a otevře pro čtení a zápisvytvoří a otevře pro čtení a zápis
abotevře pro zápis na konec souboruvytvoří a otevře pro zápis
ab+otevře pro čtení a zápis na konec souboruvytvoří a otevře pro čtení a zápis

V otevřeném souboru je jakýsi neviditelný ukazatel na aktuální pozici, čtení a zápis jej posunou o velikost přenášených dat. Tento ukazatel můžeme také posouvat explicitně, proto může mít smysl otevírat soubor například v režimu "ab+", ačkoli čtení z konce souboru smysl nemá.

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

Funkce fread a fwrite jsme si dostatečně vysvětlili již v příkladu, pouze uvedu, že velikost přenášených dat je size * nmemb. Řada programátorů to považuje za zbytečné zadávání jednoho čísla pomocí dvou parametrů a jako parametr size předávají vždy 1.

int fclose(FILE *stream);

Použití funkce fclose se od textového režimu nijak neliší.

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);

Funkcí fseek posouváme aktuální pozici v souboru a pomocí ftell ji můžeme zjistit. Parametr whence nabývá hodnot SEEK_SET, SEEK_CUR a SEEK_END podle toho, zda offset znamená posunutí od začátku, aktuální pozice nebo konce souboru, offset tedy může být i záporné číslo.

int ferror(FILE *stream);
int feof(FILE *stream);

Pomocí funkcí ferror a feof zjistíme, zda je aktuální pozice na konci souboru respektive zda došlo k chybě. Tyto funkce se používají i v textovém režimu.

int remove(const char *pathname);

Trochu stranou od ostatních uvedených funkcí je remove, která smaže soubor nebo prázdný adresář.

Všimněte si, že řadu užitečných funkcí standardní C knihovna nenabízí. Jedná se zejména o práci s adresáři nebo atributy souboru. V tomto případě obvykle programátor použije systémová volání charakteristická pro konkrétní typ operačního systémy, multiplatformní programy nad nějakou všeobjímající knihovnou typu Qt nebo WXwidgets pak volají funkce této knihovny.

Pokračování příště

V příštím dílu si ukážeme implementaci vlastních funkcí s proměnným počtem parametrů.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=899