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 | read 67156×
DISCUSSION
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ň.
mode | soubor existoval | soubor neexistoval |
rb | otevře pro čtení | chyba |
rb+ | otevře pro čtení a zápis | chyba |
wb | smaže obsah a otevře pro zápis | vytvoří a otevře pro zápis |
wb+ | smaže obsah a otevře pro čtení a zápis | vytvoří a otevře pro čtení a zápis |
ab | otevře pro zápis na konec souboru | vytvoří a otevře pro zápis |
ab+ | otevře pro čtení a zápis na konec souboru | vytvoří 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ů.