Předchozí kapitola

Obsah

Konec

Následující kapitola

8. Vstup a výstup

8.1. Standardní vstup a výstup

Standardní vstup a výstup znaků
Standardní vstup a výstup řetězců
Formátovaný standardní vstup a výstup

8.2. Vstupní a výstupní operace v paměti.
8.3.
Práce se soubory.
8.4.
Soubory s přímým voláním
8.5.
Datové proudy

Otevření a zavření proudu
Proudy a vstup/výstup znaků
Proudy a vstup/výstup řetězců
Formátovaný vstup/výstup z/do proudu
Proudy a blokový přenos dat
Další užitečné funkce
Příklady práce s proudy


Každý program zpracovává nějaká vstupní data a sděluje nám výsledky touto činností získané. Pokud by tomu tak nebylo, neměli bychom zřejmě důvod takový program vůbec aktivovat.

Vstup a výstup probíhá z různých vstupně výstupních zařízení. Jejich nejjednodušší rozdělení je znaková a bloková (blokově orientovaná). Znakové vstupní zařízení typicky představuje klávesnice, výstupní pak monitor či tiskárna. Blokové vstupně/výstupní zařízení je velmi často pružný či pevný disk. Rozdíl mezi nimi spočívá zvláště v možnostech přístupu k datům. Z klávesnice čteme sekvenčně znak po znaku (sekvenční přístup), zatímco u diskového souboru můžeme libovolně přistupovat ke zvolené části dat (používaný termín popisující tuto činnost je náhodný přístup).

Vstup a výstup budeme často zkráceně zapisovat I/O, nebo, nebude-li možnost omylu, jen IO.

Ještě než se pustíme do podrobného výkladu, definujme si některé základní pojmy.

Řádek textu je posloupnost znaků ukončená symbolem (symboly) přechodu na nový řádek. Zdůrazněme, že v uvedené definici není žádná zmínka o délce řádku. Tuto skutečnost je třeba mít na paměti.1

Soubor je posloupnost znaků (bajtů) ukončená nějakou speciální kombinací, která do obsahu souboru nepatří - konec souboru. Tuto hodnotu označujeme symbolicky EOF. Textový soubor obsahuje řádky textu. Binární soubor obsahuje hodnoty v témže tvaru, v jakém jsou uloženy v paměti počítače2. Binární soubor obvykle nemá smysl vypisovat na terminál.

8.1. Standardní vstup a výstupzpet

Každý program v jazyce C má standardně otevřen standardní vstup stdin, standardní výstup stdout a standardní chybový výstup stderr. Ty jsou obvykle napojeny na klávesnici a terminál. Na úrovni operačního systému máme ovšem možnost vstup a výstup přesměrovat. Programy, které čtou ze standardního vstupu a zapisují do standardního výstupu se často nazývají filtry.

Připomeňme si užitečné filtry grep, more, find. Na filtrech, krátkých a relativně jednoduchých programech, je založena i jedna ze základních myšlenek Unixu. Snadno zvládnutelné filtry spojené do kolony mají často stejný efekt, jako složité a rozsáhlé programy, které bývají navíc jednostranně orientované3.

Možnost přesměrování vstupu a výstupu je rovněž velmi oblíbená pro tvorbu funkčního jádra programu, který zatím nemá obsažen styk s uživatelem. Takovému programu přesměrováváme data uložená v připraveném testovacím vstupním souboru a výsledek přesměrováváme do jiného výstupního souboru.

Několik poznámek ke standardnímu I/O:

Pro ukončení vstupu z klávesnice použijeme v MS-DOSu kombinaci ctrl-z, v Unixu ctrl-d.

Standardní vstup a výstup používá vyrovnávací paměť obsahující jeden textový řádek.

Při volání funkcí standardního vstupu/výstupu musíme použít hlavičkový soubor stdio.h.

Standardní vstup a výstup znakůzpet

představuje zcela základní možnost I/O. Funkce4

int getchar(void);

přečte ze standardního vstupu jeden znak, který vrátí jako svou návratovou hodnotu. V případě chyby vrátí hodnotu EOF. Funkce

int putchar(int c);

má zcela opačnou úlohu, znak, který je jejím argumentem zapíše na standardní výstup. Zapsaná hodnota je současně návratovou hodnotou, nenastane-li chyba. Pak vrací EOF.

Zdůrazněme podstatný fakt, typ hodnoty, kterou čteme/zapisujeme je int, nikoliv char, jak bychom v první chvíli očekávali. Tuto záležitost nám objasní opětné přečtení definice souboru z úvodu této kapitoly. Soubor obsahuje znaky. To odpovídá naší představě. Ale konec souboru je představován hodnotou, která do souboru nepatří. Tato hodnota ovšem musí být odlišná od ostatních znaků. A proto je typu int.

Čtení znaků ze standardního vstupu a jejich zápis na standardní výstup ukazuje program, představující jednoduchou variantu příkazu kopírování souboru (nesmíme ovšem zapomenout přesměrovat vstup a výstup).

/****************************************/
/* CPY.C                                */
/* CoPY character                       */
/****************************************/

#include <stdio.h>

int main(void)
{
 int c;

 while ((c = getchar()) != EOF)
   putchar(c);
 return 0;
}

Standardní vstup a výstup řetězcůzpet

je jednoduchou nadstavbou nad čtením znaků. Obě funkce,

char *gets(char *s);
int puts(const char *s);

pracují s řetězci. gets načte do znakového pole vstupní řetězec až do konce řádku, symbol '\n' není do znakového pole zapsán. Ukazatel na pole (načtený řetězec) je rovněž návratovou hodnotou. Chybu signalizuje návrat NULL. puts zapíše řetězec na výstup a přidá přechod na nový řádek '\n'. Chybu představuje návratové EOF, jinak vrací kladné celé číslo.

Jednoduchost použití skrývá velké nebezpečí. Funkce gets() nemá informaci o délce oblasti vymezené pro čtený řetězec. Je-li oblast kratší, než vstupní řádek, dojde jeho načtením velmi pravděpodobně k přepsání paměťové oblasti související s vyhrazenou pamětí. A to se všemi důsledky z toho vyplývajícími.

Následující program je modifikací předchozího kopírování souborů. Je ovšem možno jej použít pouze pro textové soubory. Navíc mohou vzniknout odlišnosti mezi originálem a kopií - např. končí-li poslední řádek originálu přímo EOF a ne '\n', je konec řádku '\n' připojen díky vlastnosti funkce puts().

/****************************************/
/* GETS.C                               */
/* GET String function                  */
/****************************************/

#include <stdio.h>
#define MAX_STR 512

int main(void)
{
 char s[MAX_STR];

 while (gets(s) != NULL)
   puts(s);
 return 0;
}

Formátovaný standardní vstup a výstupzpet

V Úvodu jsme se povrchně seznámili s funkcemi pro formátovaný vstup a výstup printf() a scanf(). Tyto funkce jsou základními představiteli funkcí pro formátovaný I/O, které se sice navzájem liší zdrojem či spotřebitelem, nicméně formát argumentů a formátovací řetězce používají stejným způsobem. Proto na tomto místě podrobněji popíšeme vlastnosti (způsob volání), na něž se dále budeme odvolávat, nebo které jsou v textu použity.

Pro formátovaný standardní výstup používáme funkci:

int printf (const char *format [, argument, ...]);

První parametr format určuje formátovací řetězec. Ten může obsahovat popis formátu pro každý argument, nebo text, který bude zapsán do výstupu. Popis formátu vždy začíná znakem %. Chceme-li znak % použít jako text, zdvojíme jej: %%. Návratová hodnota reprezentuje počet znaků zapsaných do výstupu, nebo EOF v případě chyby.

Specifikace formátu má poměrně velmi rozsáhlé možnosti:

% [flags] [width] [.prec] [F|N|h|l|L] type_char

Každá specifikace formátu začíná symbolem %. Po něm mohou v uvedeném pořadí následovat další položky:

položka význam
flags zarovnání výstupu, zobrazení znaménka a desetinných míst u čísel, úvodní nuly, prefix pro osmičkový a šestnáctkový výstup
width minimální počet znaků na výstupu, mohou být uvedeny mezerami nebo nulami
.prec maximální počet znaků na výstupu, pro celá čísla minimum zobrazených znaků, pro racionální počet míst za desetinnou tečkou
F|N|h|l|L l indikuje dlouhé celé číslo, L long double, ostatní mají význam v MS-DOSu
type_char povinný znak, určuje typ konverze

Specifikujme si nyní typ konverze (type_char):

symbol význam
d, i desítkové celé číslo se znaménkem
u desítkové celé číslo se bez znaménka
o osmičkové celé číslo
x, X šestnáctkové celé číslo, číslice ABCDEF malé (x) nebo velké (X)
f racionální číslo (float, double) bez exponentu, implicitně 6 desetinných míst
e, E racionální číslo (float, double) v desetinném zápisu s exponentem, implicitně jedna pozice před des. Tečkou, 6 za. Exponent uvozuje e, resp. E (dle použitého symbolu).
g, G racionální číslo (float, double) v desetinném zápisu s exponentem nebo bez něj (podle absolutní hodnoty čísla). Nemusí obsahovat desetinnou tečku (nemá-li desetinnou část). Pokud je exponent menší, než -4, nebo větší, než počet platných číslic, je použit.
c znak
s řetězec

Příznak (flag) může být:

příznak význam
- výsledek je zarovnán zleva
+ u čísla bude vždy zobrazeno znaménko (i u kladného)
mezera pro kladná čísla vynechá prostor pro znaménko
# pro formáty o, x, X výstup jako konstanty jazyka C, pro formáty e, E, f, g, G vždy zobrazí desetinnou tečku, pro g, G ponechá nevýznamné nuly, pro c, d, i, s, u nemá význam.

Šířka (width) může být:

šířka význam
n je vytištěno nejméně n znaků zarovnaných zleva či zprava (viz příznak), doplněno mezerami
0n jako předchozí, doplněno zleva nulami
* jako šířka pole bude použit následující parametr funkce printf()

Přesnost (.prec) může být:

přesnost význam
.0 pro e, E, f nezobrazí desetinnou tečku, pro d, i, o, u, x nastaví standardní hodnoty
.n pro d, i, o, u, x minimální počet číslic, pro e, E, f počet desetinných číslic. Pro g, G počet platných míst, pro s maximální počet znaků
* jako přesnost bude použit následující parametr funkce printf()

Pro formátovaný standardní vstup použijeme funkci:

int scanf (const char *format [, address, ...]);

První argument je opět formátovací řetězec. Ten může obsahovat tři typy zpracování objektů. jsou to:

Druhý (případně třetí, čtvrtý, ...) argument je úmyslně nazván address. Jde o to, že musí určovat paměťovou oblast, do níž bude odpovídající vstupní hodnota uložena. V praxi jde nejčastěji o adresu proměnné, nebo o ukazatel na pole znaků.

Čtení ze vstupu probíhá tak, že první formátovací popis je použit pro vstup první hodnoty, která je uložena na první adresu, druhý formátovací popis je použit pro vstup druhé hodnoty uložené na druhou adresu, ... .

Návratová hodnota nás informuje kladným celým číslem o počtu bezchybně načtených a do paměti uložených položek (polí), nulou o nulovém počtu uložených položek a hodnotou EOF o pokusu číst další položky po vyčerpání vstupu.

Formát vstupní hodnoty popíšeme následovně:

% [*] [width] [F|N] [h|l|L] type_char

Po znaku % mohou následovat jednotlivé položky v uvedeném pořadí:

položka význam
* přeskoč popsaný vstup
width maximální počet vstupních znaků
F|N blízký nebo vzdálený ukazatel (jen MS-DOS)
h|l|L modifikace typu
type_char (povinný) typ konverze

Hvězdička (*) ve specifikaci formátu umožňuje příslušnou vstupní hodnotu sice ze vstupu načíst, ale do paměti nezapisovat. Šířka (width) určuje maximální počet znaků, které budou použity při vstupu. Modifikátory typu (h|l) týkající se celočíselné konverze (d) určují typ short resp. long, (l|L) modifikuje racionální typ float na double na long double.

Specifikujme si nyní typ konverze (type_char):

symbol význam
d celé číslo
u celé číslo bez znaménka
o osmičkové celé číslo
x šestnáctkové celé číslo
i celé číslo, zápis odpovídá zápisu konstanty jazyka C (např. 0x uvozuje šestnáckové č.)
n počet dosud přečtených znaků probíhajícím voláním funkce scanf()
e, f, g racionální číslo typu float, lze modifikovat pomocí l|L
s řetězec (i zde jsou úvodní oddělovače přeskočeny!), v cílovém poli je ukončen '\0',
c vstup znaku, je-li určena šířka, je čten řetězec bez přeskočení oddělovačů!
[search_set] jako s, ale se specifikací vstupní množiny znaků (je možný i interval, např. %[0-9], i negace, např. %[^a-c].

Popis formátovaného standardního vstupu a výstupu je značně rozsáhlý. Některé varianty kombinací paramertů formátu se vyskytují poměrně exoticky. Častěji se setkáme s využitím návratové hodnoty funkce scanf().

Příkladem může být cyklus, v němž načteme číselnou hodnotu (zadat můžeme celé i racionální číslo) a vypočteme a zobrazíme její druhou mocninu. Cyklus končí teprve ukončením vstupu - symbolem EOF. Tento symbol je v MS-DOSu ^Z, v Unixu ^D. Program můžeme chápat i jako malé ohlédnutí k cyklům:

/************************************************/
/* SCANWHIL.C                                   */
/* program je ukazkou nacitani pomoci scanf     */
/* v kombinaci s cyklem while.                  */
/* Ideova motivace K&R.	                        */
/************************************************/

#include <stdio.h>

int main(void)
{
 float f;

 printf("zadej x:");
 while (scanf("%f", &f) != EOF)
   {
    printf("x=%10.4f x^2=%15.4f\nzadej x:", f, f*f);
   }
 return 0;
} /* int main(void) */

zadej x:2
x=    2.0000 x^2=         4.0000
zadej x:3
x=    3.0000 x^2=         9.0000
zadej x:4
x=    4.0000 x^2=        16.0000
zadej x:^Z

8.2. Vstupní a výstupní operace v paměti.zpet

V paměti počítače můžeme provádět prakticky stejné operace, jako při standardním vstupu a výstupu. Jen musíme specifikovat vstupní respektive výstupní řetězec. Tyto operace se provádějí pomocí funkcí sprintf() a sscanf(). Úvodní s v nich znamená string a fakticky definuje rozdíl mezi nimi a rodinou funkcí scanf a printf. Podívejme se na deklarace:

int sprintf (char *buffer, const char *format [, argument, ...]);
int sscanf (const char *buffer, const char *format [, address, ...]);

Vstupní a výstupní řetězec je buffer, ostatní argumenty mají stejný význam jako u funkcí pro formátovaný standardní vstup a výstup.

8.3. Práce se soubory.zpet

V odstavci věnovaném standardnímu vstupu a výstupu jsme si ukázali, že na této úrovni můžeme vlastně pracovat i se soubory. Stačí jen na úrovni operačního systému přesměrovat vstup a/respektive výstup. Taková práce se soubory má velikou výhodu. Vždy probíhá na textové úrovni, která je implementačně nezávislá5. Zmíněná nezávislost a tím i přenositelnost sebou přináší i neustálou nutnost konverze mezi vnitřním (binárním) a vnějším tvarem hodnoty. Zpravidla se jedná o čísla. Číslo ve vnitřním tvaru má pevný formát a bez ohledu na svou hodnotu zaujímá tedy vždy6 stejnou část paměti. Tuto vlastnost řetězec, představující textový zápis čísla obvykle nesplňuje. Důvody popsané v tomto odstavci ovšem nejsou ty, pro něž je odlišen standardní IO od souborového IO.

Soubor, s nímž můžeme v jazyce C pracovat má své jméno. Tím rozumíme jméno na úrovni operačního systému7. Navíc, program může zjistit jméno souboru například až za chodu při interakci s uživatelem. Programátor může určit, zda obsah souboru bude interpretován textově, nebo binárně. Uvedené vlastnosti odlišují pojmenovaný soubor od standardního vstupu a výstupu.

Jazyk C umožňuje se soubory pracovat na dvou odlišných úrovních.

První z nich je tak zvaný přímé volání . Tím se rozumí přímé volání služeb jádra systému. Poskytuje sice maximální možnou rychlost díky vazbě na jádro systému, současně však může být systémově závislý. Tento způsob práce se soubory je definován v K&R. Je dobré znát jeho základní principy, neboť se v praxi můžeme setkat s mnoha staršími zdrojovými texty používajícími přímé volání. Soubory s přímým voláním nejsou součástí ANSI normy. Jsou však definovány normou POSIX, kde se používají pro volání služeb jádra systému.

Druhým přístupem, je datový proud. Způsob práce s ním definuje jak ANSI norma jazyka C, tak norma definující rozhraní přenositelného operačního systému POSIX (Portable Operating Systems Interface). Pro manipulaci s datovým proudem máme k dispozici řadu funkcí, které nám poskytují vysoký komfort. Navíc, díky citovaným normám, máme zajištěnu nejen existenci těchto funkcí, modifikátorů a definic, ale i jejich definované vlastnosti a rozhraní. To nás vede k použití tohoto přístupu v našich programech.

Poznamejme, že počet souborů, které můžeme v programu současně otevřít, je omezen OS (nejčasteji jeho konfigurací). Pro zjištění, jaký limit máme k dispozici, slouží makro FOPEN_MAX. Operační systém rovněž omezuje délku jména souboru. Někdy dokonce na neuvěřitelných 8 + 3 znaky. Rovněž tuto hodnotu můžeme zjistit pomocí makra, tentokrát FILENAME_MAX. Odstavec věnovaný makrům, použitelných jak ve spojení se soubory s přímým přístupem, tak s proudy, ukončeme známou konstantou EOF, představující konec souboru.

Popišme si obě možnosti práce se soubory v pořadí jejich historického vzniku. V úvodu obou pasáží nejprve shrneme nejdůležitější funkce, pak uvedeme příklady, ukazující základní principy jejich použití. Ještě jednou ovšem poznamenejme, že práce s datovými proudy zaručuje přenositelnost.

8.4. Soubory s přímým volánímzpet

Pro práci se soubory s přímým voláním musíme používat funkční prototypy umístěné v io.h., sys\stat.h a fcntl.h. Jednoznačným popisem souboru v rámci běžícího programu (procesu) je kladné celé číslo, představující číselný popis souboru - manipulační číslo (deskriptor, handle).

int open(const char *path, int access [ , unsigned mode ] );

Slouží k otevření a případnému vytvoření souboru. Vrací kladný deskriptor souboru, v případě neúspěchu -1 a nastaví kód chyby v globální proměnné errno.

Další argumenty open() jsou:

path je řetězec určující jméno souboru na úrovni OS. Může obsahovat i cestu. V případě MS-DOSu je nutné zdvojovat opačná lomítka oddělující navzájem jednotlivé podadresáře cesty - jedná se o normální céčkový řetězec, včetně interpretace znaku \.

access popisuje režim přístupu k otevřenému souboru. S výjimkou prvních tří možností je možné je navzájem kombinovat pomocí bitového součtu | .

mode je nepovinný příznak, definující atributy souboru na úrovni OS. Při jeho určení využíváme předdefinované konstanty S_I... .

příznak význam
O_RDONLY otevření pouze pro čtení
O_WRONLY otevření pouze pro zápis
O_RDWR otevření pro čtení i zápis
O_NDELAY pro kompatibilitu s UNIXem
O_APPEND připojení na konec souboru. Je-li nastaven, probíhá každý zápis na konec souboru
O_CREAT vytvoř a otevři soubor. Pokud soubor již existuje, nemá význam. Jinak je soubor vytvořen.
O_EXCL výlučné otevření. Použití jen současně s O_CREAT. Pokud soubor existuje, vrátí chybový kód.
O_TRUNC otevření s ořezáním. Pokud soubor existuje, je jeho délka nastavena na 0. Atributy souboru zůstávají nezměněny.

Příznaky pro textový/binární režim. Mohou být použity funkcemi fdopen, fopen, freopen, _fsopen, open a sopen:

příznak význam
O_BINARY explicitní otevření souboru v binárním režimu. Nejsou prováděny žádné konverze.
O_TEXT explicitní otevření souboru v textovém režimu. Jsou prováděny konverze CR-LF.

Další symbolické konstanty umožňují nastavení atributů souboru. Jsou definovány v souboru SYS\STAT.H:

příznak význam
S_IWRITE povolení zápisu
S_IREAD povolení čtení
S_IREAD|S_IWRITE povolení čtení i zápisu

Popis k jediné funkci open() je poměrně zdlouhavý. Máme však alespoň za sebou výčet předdefinovaných identifikátorů, se kterými se můžeme při práci s funkcemi pro přímý přístup k souborům setkat. Protože soubor chceme jistě nejen otevřít, ale s ním i dále pracovat, nezbývá, než si popsat i další potřebné funkce.

int creat(const char *path, int amode);

Vytváří nový soubor, nebo umožňuje přepsat soubor existující. Hodnoty amode umožňují nastavení atributů souboru podobně, jako u open(). Řetězec path představuje (úplné) jméno souboru. Vrací kladný deskriptor souboru, v případě neúspěchu -1 a nastaví kód chyby v globální proměnné errno.

int close(int handle);

Zavírá soubor, určený hodnotou deskriptoru handle. Současně vyprázdní případné vyrovnávací paměti, uzavře a uvolní související datové oblasti8. Vrací 0, proběhlo-li uzavření souboru správně. V případě neúspěchu vrací hodnotu -1.

int read(int handle, void *buf, unsigned len);

Čte ze souboru určeného manipulačním číslem handle tolik bytů, kolik je určeno hodnotou len do paměťové oblasti určené ukazatelem buf. Návratová hodnota je -1 v případě neúspěchu. Jinak je vrácen počet přečtených bytů9. Funkce neprovádí (a ostatně ani nemůže provádět) žádnou kontrolu velikosti paměťové oblasti buf a požadovaného početu bajtů pro čtení len. Pokud chceme jedním příkazem načíst více, než máme vyhrazeného prostoru, skončí takový příkaz fiaskem.

int write(int handle, void *buf, unsigned len);

Z paměťové oblasti určené ukazatelem buf zapíše do souboru určeného manipulačním číslem handle tolik bytů, kolik je určeno hodnotou len. Návratová hodnota je v případě neúspěchu -1. Jinak je vrácen počet úspěšně zapsaných bytů10. Ten by se měl shodovat s hodnotou len. Jinak může být například plný disk.

long lseek(int handle, long offset, int fromwhere);

Je funkce, která nám umožní přemístit aktuální pozici v souboru na požadované místo. A to jak pro čtení, tak pro zápis11. Hodnota handle je zřejmá. Hodnota offset udává požadovaný počet bajtů posunu. Zdůrazněme, že se jedná o hodnotu typu long int. Pro stanovení pozice fromwhere, vzhledem k níž má být posun proveden, slouží předdefinované hodnoty:

SEEK_SET posun vůči aktuální pozici
SEEK_CUR posun vzhledem počátku
SEEK_END posun vzhledem ke konci

Nedoporučuje se používat jejich skutečné číselné hodnoty (po řadě 0, 1 a 2). Návratová hodnota je opět typu long int. Představuje skutečně nastavenou pozici v souboru, ovšem vždy vzhledem k počátku! V případě chyby vrací -1L.

Pro zkrácení zavedeme pro aktuální pozici v souboru zkratku CP (od Current Pointer). CP si můžeme představit jeho čtecí/záznamovou hlavu, která se nachází nad obsahem souboru. Dále zdůrazněme, že každá operace čtení/zápisu přesune CP o příslušný počet bajtů. Pro jistotu doplňme, že směrem od začátku ke konci souboru. Rovněž musíme připomenout, že při nastaveném O_APPEND probíhá každý zápis na konec souboru. Tedy bez ohledu na původní polohu CP.

Podívejme se postupně na několik příkladů, v nichž si ukážeme základní práci se soubory s přímým přístupem.

Nejprve vytvoříme binární soubor, obsahující hodnoty druhých odmocnin čísel od 1 do 99. Tyto hodnoty budou typu float.

/************************************************/
/* soubor IO-PV01.C                             */
/* vytvori binarni soubor ODMOC.DTA, ktery      */
/* obsahuje hodnoty druhych odmocnin od 1 do 99 */
/* hodnoty jsou typu float                      */
/************************************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <math.h>
#include <sys\stat.h>

int main(void)
{
 const int mez = 100;
 int handle, i;
 char *jmeno = "odmoc.dta";
 float f;

 handle = open(jmeno, O_CREAT | O_BINARY | O_RDWR, S_IREAD | S_IWRITE);
 if (handle == -1)
   {
    printf("Chyba!\nSoubor %s nejde vytvorit.\n", jmeno);
    return(handle);
   }

 for (i = 1; i < mez; i++)
   {
    f = sqrt(i);
    if (write(handle, &f, sizeof(float)) != sizeof(float))
      {
       printf("Chyba pri zapisu %2d. hodnoty\n", i);
       return(1);
      }
   } /* for (i = 1; i < mez; i++) */
 close(handle);
 return(0);
} /* int main(void) */

Jelikož nám deklarace a definice proměnných a konstant nečiní potíže, podívejme se na klíčové příkazy. Vcelku přirozeně očekáváme, že nejprve musíme soubor vytvořit12 a otevřít jej pro zápis. Můžeme tak učinit jak funkcí creat(), tak následovně:

handle = open(jmeno, O_CREAT | O_BINARY | O_RDWR, S_IREAD | S_IWRITE);

Vytváříme (O_CREAT) nový soubor s názvem určeným řetězcem jméno (pokud existuje, je zkrácen na délku 0), který bude obsahovat binární (O_BINARY) data (tedy žádné textové konverze CR-LF) a do tohoto souboru budeme zapisovat, případně z něj máme možnost i číst (O_RDWR). Na úrovni OS požadujeme, aby soubor měl atributy umožňující jeho vlastníkovi čtení i zápis (S_IREAD | S_IWRITE). Tento třetí argument je sice nepovinný, ovšem jeho určením se vyvarujeme nedefinovaného chování některých překladačů. Pokud je vše v pořádku, máme nyní manipulační číslo souboru handle, jinak ohlásíme chybu a ukončíme chod programu.

Dále v cyklu do našeho otevřeného souboru - první argument funkce write() obsahuje manipulační číslo souboru handle,

if (write(handle, &f, sizeof(float)) != sizeof(float))

zapisujeme obsah proměnné f. Tento moment je velmi významný. Nestačí totiž jen určit, že obsah proměnné se nachází na adrese &f. Musíme ještě určit, kolik bajtů má býti zapsáno. Činíme tak třetím argumentem sizeof(float) funkce write(). Tím sice máme zápis popsán, my však raději ještě provedeme kontrolu, bylo-li přeneseno právě tolik bytů, kolik jsme požadovali. Vyhodnocení případné nerovnosti je v programu současně chybovou zprávou a následným ukončením programu.

Po patřičném počtu opakování, které zajistí cyklus, musíme před ukončením programu ještě soubor uzavřít:

close(handle);

Teprve poté si můžeme být jisti, že jsme pro zdárný průběh našeho prvního setkání se soubory učinili vše potřebné.

V pracovním adresáři máme vytvořen soubor odmoc.dta. Měl by být dlouhý 396 bajtů ( 99 hodnot typu float - tedy každá 4 bajty). Je-li tomu tak, můžeme s tímto souborem dále pracovat. Potřebujeme si ukázat čtení a přemístění CP v souboru.

V binárním souboru odmoc.dta máme zapsány hodnoty druhých odmocnin čísel od 1 do 99. Podívejme se nyní na program, který podle zadané hodnoty buď zobrazuje hodnoty druhé odmocniny pro zadané celé číslo ve zmíněném intervalu, nebo ukončí svou činnost. Poznamejme, že se hodnoty druhých odmocnin nepočítají, nýbrž čtou ze souboru13. Úloha tedy spočívá v umístění CP na vhodnou pozici v souboru a následném načtení a zobrazení zapsané racionální hodnoty. Celý program může vypadat takto:

/************************************************/
/* soubor IO_PV02.C                             */
/* cte binarni soubor ODMOC.DTA, ktery obsahuje */
/* hodnoty druhych odmocnin od 1 do 99          */
/* hodnoty jsou typu float                      */
/* hledanou hodnotu odmocniny najde v souboru   */
/* pomoci lseek                                 */
/************************************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>

int main(void)
{
 const int mez = 100;
 int handle, i;
 char *jmeno = "odmoc.dta";
 float f;

 handle = open(jmeno, O_BINARY | O_RDONLY);
 if (handle == -1)
   {
    printf("Chyba!\nSoubor %s nejde cist.\n", jmeno);
    return(handle);
   }
 printf("zadej hodnotu <1..%3d> :", mez - 1);
 scanf("%d", &i);
 while ((i >= 1) && (i < mez))
   {
    if (lseek(handle, ((long) i - 1l) * sizeof(float), SEEK_SET) == -1l)
      {
       printf("Chyba pri nastaveni na %2d. polozku\n", i);
       return(1);
      }

    if (read(handle, &f, sizeof(float)) != sizeof(float))
      {
       printf("Chyba pri cteni %2d. hodnoty\n", i);
       return(1);
      }
    printf("2. odmocnina z %3d je %10.4f\n\n", i, f);
    printf("zadej hodnotu <1..%3d> :", mez - 1);
    scanf("%d", &i);
   } /* while ((i >= 1) && (i < mez)) */
 close(handle);
 return(0);
} /* int main(void) */

Opět se zaměříme na nové funkce případně na jejich nové použití. Nejprve musíme otevřít existující soubor. Víme, že z něj budeme pouze číst, tedy:

handle = open(jmeno, O_BINARY | O_RDONLY);

Tentokrát testujeme návratovou hodnotu (a tedy úspěšnost otevření souboru) až na následujícím řádku. Dále potřebujeme přemístit CP na správnou pozici v souboru,

if (lseek(handle, ((long) i - 1l) * sizeof(float), SEEK_SET) == -1l).

První argument lseek(), tedy manipulační číslo souboru, získáme při otevření. Následuje zdánlivě složitý výpočet pozice, na níz chceme CP přemístit. Třetí SEEK_SET argument říká, že pozice bude vzhledem k počátku souboru. Odbočme na chvíli. První položka souboru má posun nula. Druhá položka má posun jedna krát délka položky. Třetí ... . Poslední položka, řekněme n-tá, má posun (n - 1) krát délka položky.

Nyní již snadno nahlédneme, že výpočet ((long) i - 1l) * sizeof(float) skutečně určuje správnou pozici CP pro načtení hodnoty druhé odmocniny. Ty jsou v souboru přece pro celá čísla 1 až 99. Přetypování proměnné i na long je spíše proto, abychom si uvědomili typ tohoto argumentu.

CP je správně nastaven, nezbývá nám, než načíst potřebný počet bajtů na správné místo,

if (read(handle, &f, sizeof(float)) != sizeof(float))

pro jistotu i s kontrolou na skutečně přečtený počet bajtů. A to je v podstatě vše. Po zadání čísla mimo rozsah je program ukončen. Předtím pochopitelně nezapomeneme uzavřít soubor.

Než přejdeme k ukázce příkladu práce s textovými soubory s přímým přístupem, ukažme si, jak zjistit informaci, která nezávisí na typu dat v souboru uložených. Touto informací je jistě délka souboru (počet bajtů). Můžeme se pustit na tenký led a použít specialisované funkce záviské na konkrétním OS. Nebo si uvědomíme, jakou návratovou hodnotu má funkce lseek(). Je to přece pozice v souboru vzhledem k jeho počátku. Nastavíme-li tedy CP na konec souboru, získáme požadovanou informaci. Jak provedeme nastavení na konec? Snadno,

if ((l = lseek(handle, 0l, SEEK_END)) == -1l)

prostě provedeme v otevřeném souboru přesun o nula bajtů, ovšem vzhledem k jeho konci. Výpis celého programu, s mírným ošetřením některých možných zdrojů chyb, následuje:

/************************************************/
/* soubor IO-PV03.C                             */
/* Zjisti a vytiskne delku souboru, jehoz jmeno */
/* je zadano z klavesnice.                      */
/************************************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>

int main(void)
{
 int handle;
 char jmeno[80];
 long l;

 printf("\nzadej jmeno souboru :");
 scanf("%s", jmeno);

 handle = open(jmeno, O_BINARY | O_RDONLY);
 if (handle == -1)
   {
    printf("Chyba!\nSoubor <%s> nejde cist.\nNeexistuje?\n", jmeno);
    return(handle);
   }

 if ((l = lseek(handle, 0l, SEEK_END)) == -1l)
   {
    printf("Chyba pri nastaveni na konec souboru <%s>\n", jmeno);
    return(1);
   }
 else
   {
    printf("Soubor <%s> je dlouhy %ld bajtu.\n", jmeno, l);
    close(handle);
    return(0);
   }
} /* int main(void) */

Je načase ukázat si i práci s textovými soubory pomocí funkcí pro přímý přístup. Naším úkolem bude vytvořit několikařádkový textový soubor. Ukážeme si, jak do něj zapsat řetězce a co musíme udělat proto, chceme-li do textového souboru zapsat i číselné hodnoty. V podstatě se nejedná o nic převratného. Jen při otevření či vytvoření souboru musíme určit, že jej budeme chápat jako textový,

if ((handle = open(jmeno, O_TRUNC | O_CREAT | O_TEXT | O_WRONLY, S_IWRITE)) == -1)

Proto tedy hodnota O_TEXT. Navíc zde máme kombinaci příznaků O_TRUNC | O_CREAT, která zajistí nejen vytvoření souboru, ale v případě jeho existence i zkrácení na nulovou délku (tedy zrušení starého obsahu). Příkaz je doplněn i příznakem O_WRONLY, dávajícím informaci, že do souboru hodláme výhradně zapisovat. Třetím argumentem funkce pro otevření, příznakem S_IWRITE, sdělujeme prakticky totéž OS.

Vyvstává problém. Při zápisu do souboru musíme znát počet bajtů, které chceme přenést. Máme, jako již tradičně, více možností, jak tuto informaci získat. Postup

write(handle, "Uz to umim!\n", strlen("Uz to umim!\n"));

jistě není příliš vhodný. Řetězec musíme uvést dvakrát. Nejenže tedy je pravděpodobně umístěn dvakrát v paměti (pokud náš překladač při optimalizaci nezjistí shodu literálů). Navíc v případě, kdy chceme řetězec změnit, musíme tuto změnu identicky provést na dvou místech. Zvolené řešení, kdy do vytvořeného prostoru char s[81] řetězec prostě nakopírujeme pomocí strcpy(), vede ve všech případech zápisů ke shodnému a poměrně čitelnému

write(handle, s, strlen(s));

Po krátkém zamyšlení ovšem opět nahlédneme, že řetězec je v paměti opět umístěn dvakrát. Navíc jej ještě kopírujeme! Proč jsme tedy volili tuto na první pohled strašlivou variantu?14 Odpoví nám dvojice následujících řádků

sprintf(s, "%5d", i);
write(handle, s, strlen(s));,

které jsou umístěny v těle cyklu. Pro zobrazení číselných hodnot musíme použít u textových souborů s přímým přístupem umělý krok. Číslo nejprve převedeme z vnitřního tvaru na řetězec a teprve poté tento řetězec (reprezentující číslo ve zvoleném formátu) zapíšeme do souboru. A jestliže pro převod potřebujeme vyhrazený prostor, tak jsme tento prostor využili i výše. Odměnou za přece jen nadbývající kopírování nám může být identický zápis příkazů výstupu v obou případech.

Nyní již následuje souvislý výpis programu:

/************************************************/
/* soubor IO-PV04.C                             */
/* vytvori  textovy soubor SOUBOR.TXT           */
/* verze s primym pristupem - open              */
/************************************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
#include <string.h>
#include <sys\stat.h>

int chyba(int er)
{
 extern int errno;
 if (er != 0)
   {
    perror(strerror(errno));	/* zapise chybove hlaseni do stderr */
		exit(errno);		/* -> OS; ukonci chod programu	    */
   }
 return er;
} /* int chyba(void) */

int main(void)
{
 int handle, i;
 char s[81], *jmeno = "soubor.txt";

 if ((handle = open(jmeno, O_TRUNC | O_CREAT | O_TEXT | O_WRONLY, S_IWRITE)) == -1)
   chyba(handle);

 strcpy(s, "Toto je textovy soubor vytvoreny v C.\n"
	     "primy pristup - handle\n");
 write(handle, s, strlen(s));
 for (i = 1; i < 10; i++)
   {
    sprintf(s, "%5d", i);
    write(handle, s, strlen(s));
   } /* for (i = 1; i < 10; i++) */

 strcpy(s, "\n\n");
 write(handle, s, strlen(s));

 strcpy(s, "Uz to umim!\n");
 write(handle, s, strlen(s));

 strcpy(s, "Jeste pridat posledni radek na konec.");
 write(handle, s, strlen(s));

 if (close(handle) == -1)
   chyba(1);

 return 0;
} /* int main(void) */

Program obsahuje i funkci chyba(), o níž jsme se dosud nezmínili. Ta pracuje s globální proměnnou errno, a pomocí funkce perror() zobrazí před ukončením programu textový popis chyby. Výstup je směrován na standardní výstupní chybové zařízení, jímž je obvykle terminál. Co se popisu chyby týče, je jemnost jejího rozlišení dána nabídkou hodnot errno. Textové řetězce standardní knihovny jsou zpravidla jednojazyčné (anglické). I to je jeden z důvodů. proč jsme funkci chyba() vůbec vytvořili. Není přece žádný problém dát do těla této funkce (nejlépe) statické pole řetězců s chybovými hlášeními podle našich potřeb. Z úsporných důvodů nebudeme uvádět kód funkce chyba() v dalších výpisech programů, a to až do konce této kapitoly.

Text musíme být schopni do textového souboru nejen zapsat, ale následovně jej pak i zpracovat. Obsah vytvořeného textového soubor zobrazíme pomocí následujícího programu. Podívejme se opět na jeho klíčové části. Na rozdíl od binárních souborů, obsahujících třeba jen hodnoty typu float, obsahuje textový soubor (zpravidla) nestejně dlouhé řádky textu ukončené oddělovačem - koncem řádku. Pokud otevřeme soubor jako textový, nemusí nás zajímat, jaký znak, či znaky, konec řádku tvoří. O jeho rozpoznání se postará standardní knihovna. Ovšem přesto před námi stojí problém. Je jím délka řádku. Tu zpravidla dopředu neznáme. Ale již víme, že pouhé čtení znaků do vymezené oblasti paměti, dokud nenarazíme na konec řádku, může být velmi nebezpečné. Proto budeme raději testovat skutečně přečtený počet znaků vzhledem ke stanovenému limitu. Volba limitu je často dána obvyklou délkou řádku znakového terminálu (plus malá rezerva). Často se pohybuje mezi osmdesáti až sto znaky15.

Opusťme úvahy a podívejme se na záhlaví funkce cti_radek():

int cti_radek(int hdl, char *radek, int max)

Obsahuje tři argumenty potřebné pro určení souboru, vyrovnávací paměti a její délky (limitu). Návratová hodnona vypovídá o výsledku čtení. Je popsána ve výpisu programu. Dále si povšimneme nutnosti ukončení načteného řetězce zarážkou:

s[i] = '\x0';

Nyní se podívejme na výpis zdrojového textu:

/********************************/
/* soubor IO-PV05.C             */
/* precte a vytiskne obsah      */
/* textoveho souboru SOUBOR.TXT */
/* pouziva UNIXove funkce       */
/********************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
#include <sys\stat.h>

#define MAX_DELKA_RADKU 81 /* plus jedna */

int cti_radek(int hdl, char *radek, int max)
/************************************************/
/* Do *radek nacte ze souboru hdl radek textu   */
/* dlouhy nejvice max znaku.                    */
/* vrati: -1 pri neuspesnem cteni               */
/*         0 pri konci souboru                  */
/*           +n pri uspechu (pocet znaku)       */
/*           vyjimka (prazdny radek)  n == 1    */
/************************************************/
{
 int i = 0, kod;
 char *s = radek, ch = 0;

 while ((i < max)		/* neprekrocena max. delka	*/
	&& ((kod = read(hdl, &ch, sizeof(char))) == sizeof(char))
		/* ^ korektne nacteno a dosud neni konec	*/
	&& (ch != '\n'))	/* neni konec radku		*/
   {
    s[i] = ch;
    i++;
   }

 s[i] = '\x0';
 return (kod > 0) ? ((i == 0) ? kod : i) : kod;
} /* int cti_radek(int hdl, char *radek, int max) */

int chyba(int er)
{
 extern int errno;
 if (er != 0)
   {
    perror(strerror(errno));
    exit(errno);
   }
 return(0);
} /* int chyba(void) */

int main(void)
{
 int handle, nacteno;
 char s[MAX_DELKA_RADKU], *jmeno = "soubor.txt";

 if ((handle = open(jmeno, O_TEXT, S_IREAD)) == -1)
   chyba(handle);


 printf("\nCteni textoveho souboru <%s> funkcemi s primym pristupem\n", jmeno);
 while ((nacteno = cti_radek(handle, s, sizeof(s) - 1)) > 0)
   {
    printf("%s\n", s);
   }

 if (nacteno == 0)
   printf("%s\n\n...KONEC...\n", s);
 else
   chyba(nacteno);

 if (close(handle) == -1)
   chyba(1);

 return(0);
} /* int main(void) */

Výstup tohoto programu na obrazovku je následující16:

Cteni textoveho souboru <soubor.txt> funkcemi s primym pristupem
Toto je textovy soubor vytvoreny v C.
primy pristup - handle
    1    2    3    4    5    6    7    8    9

Uz to umim!
Jeste pridat posledni radek na konec.

...KONEC...

8.5. Datové proudyzpet

Pro práci s datovými proudy musíme používat funkční prototypy umístěné v stdio.h. Základem pro přístup k proudu je datový typ FILE17. Při každém spuštení programu máme otevřeny proudy, které bychom mohli deklarovat takto:

FILE *stdin;
FILE *stdout;
FILE *stderr;

Při práci s proudy se doporučuje testovat hodnoru TMP_MAX. Toto makro je rozšířením možností společných pro obě varianty práce se soubory.

ANSI norma definuje dva režimy proudů - textový (rozlišuje řádky) a binární. Režim stanovíme při otevírání souboru. Pro další výklad bude zřejmě opět nejvhodnější, uvedeme-li si nejprve nejdůležitější funkce a konstanty pro práci s proudy, a teprve poté několik příkladů, které výčet osvětlí. Pro přehlednost uvedeme skupiny funkcí, rozdělené podle možnosti jejich použití.

Pro práci s proudy máme k dispozici značné množství funkcí, maker a globálních proměnných. Jejich plný výčet je nad rámec tohoto textu. Proto se omezíme na ty z nich, které považujeme za významné. Ostatní můžeme vyhledat v referenčních příručkách C, nebo v elektronické dokumentaci přímo u počítače.

Otevření a zavření prouduzpet

Každý proud máme možnost otevřít a uzavřít. Při otevření určujeme režim našeho přístupu k datům v proudu. Ve speciálních případech oceníme i možnost proud znovuotevřít či spojit s novým souborem.

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

Je funkce, vracející ukazatel na strukturu FILE v případě úspěšného otevření proudu. Při neúspěchu vrací hodnotu NULL. Konstantní řetězec filename, označuje jméno souboru podle konvencí příslušného OS. Řetězec mode určuje režim práce se souborem i jeho typ. Základní možnosti jsou uvedeny v tabulce:

řetězec význam (otevření pro:)
r čtení
w zápis
a připojení
r+ aktualizace (update) - jako rw
w+ jako výše (r+), ale existující proud ořízne na nulovou délku, jinak vytvoří nový
a+ pro aktualizaci, pokud neexistuje, vytvoří
t textový težim
b binární režim

Poznamenejme, že otevření proudu v binárním režimu vyžaduje vždy písmeno b v řetězci mode, například: "a+b", "wb", ... . Obdobně pro textový režim je vhodné uvést jako součást řetězce mode znak t.18

int fclose(FILE *stream);

Je funkce uzavírající určený proud. V případě úspěchu vrátí hodnotu 0, jinak EOF. Uvolní paměť vyhrazenou pro strukturu FILE * a vyprázdní případnou vyrovnávací paměť.

FILE *freopen(const char *filename, const char *mode, FILE *stream);

Funkce uzavře soubor asociovaný s proudem stream (jako při volání fclose()). Pak otevře soubor jménem filename (jako při fopen(filename, mode)). Návratovou hodnotou je v případě úspěchu otevřený proud, jinak nulový ukazatel NULL.

Proudy a vstup/výstup znakůzpet

Znak je po načtení z proudu konvertován bez znaménka na typ int. Obdobně je při zápisu do proudu konvertován opačným postupem. Tak máme ponechánu možnost rozlišit konec souboru od dalšího načteného znaku.

Připomeňme si čtení a zápis znaku z/do standardního vstupu/výstupu. Při pohledu na následující funkce je význam maker getchar() a putchar() zcela zřejmý. Stačí prostě doplnit druhý argument odpovídající funkce hodnotou stdin či stdout.

int getc(FILE *stream);

V případě úspěšného načtení znaku z proudu jej bez znaménka převede na typ int. Takto získaná hodnota je hodnotou návratovou. V případě chyby, nebo dosažení konce proudu pro getc(), vrací EOF.

int ungetc(int c, FILE *stream);

Je-li c různé od EOF, uloží jej funkce ungetc() do datového objektu s adresou stream a případně zruší příznak konce souboru. Následným čtením z tohoto proudu získáme námi zapsanou hodnotu. Je-li c rovno EOF, nebo nemůže-li zápis proběhnout, vrací funkce EOF. Jinak vrací c (přesněji (unsigned char) c).

int putc(int c, FILE *stream);

Zapíše znak c do proudu stream. Vrátí stejnou hodnotu, jako zapsal. V případě chyby, nebo dosažení konce proudu pro getc(), vrací EOF.

Proudy a vstup/výstup řetězcůzpet

Z proudu nemusíme číst pouze jednotlivé znaky. Můžeme načítat i celé řádky19. Ty jsou ukončeny přechodem na nový řádek. Pro vyšší bezpečnost musíme při čtení uvést velikost vyrovnávací paměti. Při zápisu to pochopitelně nutné není. Do proudu se zapíše celý řetězec až po koncovou zarážku (ovšem bez ní).

char *fgets(char *s, int n, FILE *stream);

Načte řetězec (řádek až po jeho konec) z proudu stream do vyrovnávací paměti s, nejvýše dlouhý n-1 znaků. Vrátí ukazatel na tento řetězec (vyrovnávací paměť), nebo, při chybě, NULL.

int fputs(const char *s, FILE *stream);

Zapíše do proudu řetězec ukončený zarážkou. Ani zarážku, ani případný konec řádku (obsažený na konci řetězce) do proudu nezapíše. V případě úspěchu vrátí počet zapsaných znaků (délku řetězce), jinak EOF.

Formátovaný vstup/výstup z/do prouduzpet

Jestliže jsme zvládli standardní formátovaný vstup a výstup, nemůže nám činit formátovaný vstup a výstup z a do proudu potíže. Funkce se navzájem liší pouze úvodním f a identifikací proudu jako prvního argumentu. Právě srovnání je důvodem, proč uvádíme odpovídající funkce ve dvojici.

int fprintf (FILE *stream, const char *format [, argument, ...]);
int printf ( const char *format [, argument, ...]);

int fscanf (FILE *stream, const char *format [, address, ...]);
int scanf ( const char *format [, address, ...]);

Proudy a blokový přenos datzpet

Blokový přenos dat je nezbytný při práci s binárním proudem. Pokud srovnáme tyto funkce s odpovídajícími funkcemi pro soubory s přímým přístupem, pochopíme, jak se navzájem liší. Funkcí pro přímý přístup je poměrně málo. Představují základní operace, které při práci programátor potřebuje. A to je vše. Funkce pro práci s proudy umí všelijaké jemnůstky. Je jich díky tomu poměrně mnoho a programátor se musí umět rozhodnout, kterou z nich pro daný účel použije. Některé jejich argumenty můžeme dokonce považovat za nadbytečné. Zmiňujeme se o tom právě zde, neboť to můžeme názorně dokumentovat. Při blokovém proudovém IO musím určit, kolik položek chci přenést a jaká je velikost jedné položky. Prostý součin těchto hodnot by jistě každý zvládnul20. Počet argumentů by se o jeden snížil.

Typ size_t, použitý v následujících funkcích, je zaveden pro určení velikosti paměťových objektů a případně počtu. Dává nám rovněž najevo, že je implementačně závislý. Kdyby místo něj bylo pouze unsigned int, nemuseli bychom si hned uvědomit, že v 16-ti bitovém systému je velikost položky omezena na 65535 bajtů.

size_t fread(void *ptr, size_t size, size_t n, FILE *stream);

Přečte z proudu stream položky o velikosti size v počtu n jednotek do paměťové oblasti určené ukazatelem ptr. V případě úspěchu vrátí počet načtených položek. Jinak vrátí počet menší (pravděpodobně to bude nula). Menší návratovou hodnotu, než je zadaný počet položek, můžeme získat například při dosažení konce proudu před načtením požadovaného počtu položek. Načtené položky jsou platné.

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

Tato funkce má argumenty obdobného významu, jako funkce předchozí. Pochopitelně s tím, že provádí zápis položek do proudu.

Další užitečné funkcezpet

Protože je počet dosud neuvedených funkcí pro práci s proudy velmi značný, shrneme pod názvem Další užitečné funkce alespoň některé z nich.

int feof(FILE *stream);

Je funkce, umožňující zjistit dosažení konce proudu. Její návratová hodnota je - true (t.j. různá od nuly), nacházíme-li se na konci proudu, nebo - false (nula) - jinak.

int fflush(FILE *stream);

Má-li proud přiřazenu vyrovnávací paměť, provede její vyprázdnění (zápis) do souboru. Jinak neprovede nic. Návratová nula svědčí o úspěchu, EOF signalizuje chybu.

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

Přenese aktuální pozici CP v proudu stream na stanovené místo. To je určeno posunem offset vzhledem k počátku, aktuální pozici, nebo konci souboru. Vztažný bod určuje argument whence. Předdefinované konstanty jsou stejné, jako v případě přímého přístupu - tj. lseek() (jsou to SEEK_SET, SEEK_CUR, SEEK_END). Poznamejme, že použití této funkce zruší účinek bezprostředního předešlého ungetc().

long ftell(FILE *stream);

Vrátí aktuální pozici v proudu. To je pro binární proud počet bajtů vzhledem k začátku. V případě chyby vrátí -1L a nastaví globální proměnnou errno na kladné celé číslo.

Jen zkratkovitě uvedeme ještě některé další užitečné funkce:

ferror() informuje o případné chybě při práci s proudem.

clearerr() ruší nastavení příznaku chyby a konce proudu.

perror() zobrazí řetězec chybového hlášení na standardní chybové zařízení (obvykle je to konsola).

tmpfile() otevře přechodný soubor v binárním režimu pro aktualizaci. S přechodnými soubory jsou spjaty ještě funkce tmpnam() a tempnam().

Dvojice fgetpos() a fsetpos() umožňuje uchovat (získat) pozici v proudu a pak ji (opětně) nastavit.

setbuf() a setvbuf() umožňují nastavit a případně modifikovat velikost vyrovnávací paměti pro určený proud.

Příklady práce s proudyzpet

Následující příklad vychází ze stejného zadání, jako příklad obsažený ve zdrojovém textu io-pv04.c. Naším úkolem je vytvořit několikařádkový textový soubor. Ukážeme si, jak do něj zapíšeme řetězce i číselné hodnoty.

Nejprve musíme vytvořit datový proud

if ((soubor = fopen(jmeno, "wt")) == NULL) chyba(1);

Řetězec jmeno udává jeho jméno pod OS. Další argument funkce fopen(), řetězec "wt", určuje způsob otevření a režim práce s proudem (proud vytvoříme a otevřeme pro zápis v textovém režimu). Ošetřujeme současně chybový stav, indikovaný návratovým NULL.

Při zápisu řetězce do proudu nemáme žádné problémy. Díky existenci zarážky nemusíme ani udávat jeho délku, stačí napsat

fputs(s, soubor);

Při zápisu čísla do proudu ovšem stejnou funkci použít nemůžeme. Víme totiž, že číslo je v paměti v uloženo v binárním tvaru, zatímco proud má textový režim. S výhodou použijeme funkci pro formátovaný výstup do proudu,

fprintf(soubor, "%5d", i);

její použití je obdobné, jako při formátovaném standardním výstupu. Pouze je navíc uvedeno určení proudu, jehož se výstup týká.

Po ukončení práce nesmíme zapomenout proud uzavřít,

if (fclose(soubor) == EOF) chyba(1);

Po popisu klíčových řádků se podívejme na souvislý výpis zdrojového textu programu:

/************************************************/
/* soubor IO-DP01.C                             */
/* vytvori  textovy soubor SOUBOR.TXT           */
/* verze se streamem                            */
/************************************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
#include <string.h>
#include <sys\stat.h>

int chyba(int er)
{
 extern int errno;
 if (er != 0)
   {
    perror(strerror(errno));  /* zapise chybove hlaseni do stderr */
    exit(errno);              /* -> DOS; ukonci chod programu     */
   }
 return er;
} /* int chyba(void) */

int main(void)
{
 FILE *soubor;
 int i;
 char *s, *jmeno = "soubor.txt";

 if ((soubor = fopen(jmeno, "wt")) == NULL)
        chyba(1);

 s = "Toto je textovy soubor vytvoreny v C.\n"
     "pristup pomoci proudu - FILE *\n";
 fputs(s, soubor);
 for (i = 1; i < 10; i++)
        {
         fprintf(soubor, "%5d", i);
        } /* for (i = 1; i < 10; i++) */

 fputs("\n\n", soubor);

 s = "Uz to umim!\n";
 fputs(s, soubor);

 s = "Jeste pridat posledni radek na konec.";
 fputs(s, soubor);

 if (fclose(soubor) == EOF)
        chyba(1);

 return 0;
} /* int main(void) */

Povšimněme si ještě zajímavé skutečnosti. Posledním příkazem fputs() zapíšeme do proudu řetězec, nikoli ovšem symbol konce řádku. Protože se jedná o poslední text, umístěný do souboru, bude užitečné si říci, že konec řádku je buď přechod na řádek nový, nebo konec souboru.

Opačnou úlohou, než je předchozí tvorba a zápis do textového souboru, může být jeho načtení a zobrazení obsahu. Otevření textového proudu pro čtení nám jistě vrásky na čele neudělá. Pro určení režimu zadáme "rt" - otevíráme existující textový soubor pro čtení. Dále je úloha zdánlivě rovněž jednoduchá. Stačí přeci v cyklu číst celé řádky a zobrazovat jejich obsah. Například takto:

while (fgets( s, sizeof(s) - 1, soubor) != NULL)

Vše bude fungovat. Jen poslední informace, zapsaná do souboru, nebude zobrazena. Naopak uzříme hlášení o chybě. Problém tkví v tom, že fgets() při posledním čtení (správně) vrátí NULL a my nezískáme závěrečný text. Naše řešení je následující:

while ((nacteno = cti_radek(soubor, s, sizeof(s) - 1)) != EOF)

I když je na první pohled zřejmé, nemůžeme vynechat popis funkce cti_radek(). Její záhlaví je díky mnemonicky pojmenovaným argumentům samopopisné.

int cti_radek(FILE *f, char *radek, int max);

Prakticky (až na pořadí) je shodné s fgets(). Vzhledem k našemu problému se musí lišit ve způsobu reakce na konec souboru a v návratové hodnotě. Nejprve návratová hodnota. Vnořené (dvojité) použití podmíněného operátoru by pro nás mělo být čitelné. Možné návratové hodnoty tedy jsou:

EOF při chybě při čtení

n - (kladné celé číslo) - počet načtených znaků řetězce

výjimka: prázdný řádek (pouze ukončení řádku) n == 1.

Kód této funkce nám ukáže, kolik toho za nás provádí standardní knihovní funkce fgets(). Celý zdrojový text řešení našeho příkladu následuje.

/*******************************/
/* soubor IO-DP02.C            */
/* precte a vytiskne obsah     */
/* textoveho souboru SOUBOR.TXT*/
/* verze pouziva stream        */
/* pouziva UNIXove funkce      */
/*******************************/

#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
#include <sys\stat.h>
#include <string.h>

#define MAX_DELKA_RADKU 81 /* plus jedna */

int cti_radek(FILE *f, char *radek, int max)
/************************************************/
/* Do *radek nacte ze streamu f radek textu     */
/* dlouhy nejvice max znaku.                    */
/* vrati: EOF pri neuspesnem cteni              */
/*       +n pri uspechu (pocet znaku)           */
/*       vyjimka - prazdny radek - n == 1       */
/************************************************/
{
 int i = 0, ch = 0;
 char *s = radek;
 while ((i < max)           /* neprekrocena max. delka    */
       && ((ch = getc(f)) != EOF)
              /* ^ korektne nacteno a dosud neni konec    */
       && (ch != '\n'))     /* neni konec radku           */
   {
    s[i] = ch;
    i++;
   }

 s[i] = '\x0';
 return (ch == EOF) ? ch : ((i == 0) ? 1 : i);
} /* int cti_radek(int hdl, char *radek, int max) */

int main(void)
{
 int nacteno;
 FILE *soubor;
 char s[MAX_DELKA_RADKU], *jmeno = "soubor.txt";

 if ((soubor = fopen(jmeno, "rt")) == NULL)
   chyba(1);

 printf("\nCteni textoveho souboru <%s> funkcemi pro datovy proud\n", jmeno);

 while ((nacteno = cti_radek(soubor, s, sizeof(s) - 1)) != EOF)
  {
   printf("%s\n", s);
   *s = 0x0;
  }

 if ((nacteno == EOF) && (strlen(s) != 0))
   printf("%s\n\n...KONEC...\n", s);
 else
   chyba(nacteno);

 if (fclose(soubor) == -1)
   chyba(1);

 return 0;
} /* int main(void) */

A takto vypadá výstup programu IO-DP01.C zobrazený pomocí programu IO-PV02.C:

Cteni textoveho souboru <soubor.txt> funkcemi pro datovy proud
Toto je textovy soubor vytvoreny v C.
pristup pomoci proudu - FILE *
    1    2    3    4    5    6    7    8    9

Uz to umim!
Jeste pridat posledni radek na konec.

...KONEC...

Náměty pro další možné pokusy při práci s proudy můžeme čerpat z příkladů napsaných pro ukázky práce se soubory s přímým přístupem. I bez jejich přepsání a komentování je tato kapitola velmi rozsáhlá.


Výsvětlivky:

1 Naše programy by se měly rozumně chovat pro vstupní řádky libovolné délky. Nicméně za slušné chování se obvykle pokládá přijetí řádku do nejvýše 512 znaků, jak činí mnohé Unixové programy.
2 OS nezná typ souboru. Přípony jmén souborů jsou většinou pouze doporučeními. Proto záleží jen na nás, v jakém režimu chceme k souboru přistupovat.
3 Tím máme na mysli, že výběr a vzájemné spojení funkcí je dáno autorem takového programu. Unixovské filtry si propojíme sami.
4 Ve skutečnosti jsou jak getchar tak putchar makra, představující volání odpovídajících funkcí pracujících se standardním vstupním a výstupním proudem - getc(stdin) a putc(c, stdout).
5 Pokud nebereme v úvahu dvě možná kódováni ASCII a EBCDIC, neboť druhé se v MS-DOSu ani v Unixu nepoužívá.
6 Máme pochopitelně na mysli libovolný pevně zvolený datový typ.
7 Někdy se v této souvislosti zavádí pojmy vnější a vnitřní jméno souboru. Tím se rozumí jméno souboru na úrovni OS a jednoznačná identifikace souboru v rámci programu (procesu).
8 Návod na ověření uvolňování hodnot manipulačních čísel souboru je následující. Otevíráním nových souborů se zvyšuje jejich manipulační číslo. Pokud uzavřeme některý z otevřených souborů a posléze otevřeme soubor nový, může dostat stejné manipulační číslo, jaké měl soubor uzavřený.
9 Chceme-li například přečíst posledních 100 bajtů ze souboru, kde však zbývá již jen 50 bajtů, není to chyba. Funkce read() prostě vrátí hodnotu 50. Skutečně přečtený počet bajtů. Z jejího pohledu se o chybu nejedná.
10 Nezapomínejme, že naše data jsou zřejmě uložena ve vyrovnávací paměti. Teprve po close() máme jistotu, že vyrovnávací paměť pro příslušný soubor je vyprázdněna.
11 Pokud například potřebujeme vytvořit soubor dlouhý 500000 bajtů, máme dvě možnosti. Buď budeme otrocky zapisovat například 1000krát 500 bajtů. Nebo po otevření souboru použijeme lseek(handle, 500000L, SEEK_SET) a je to. V prvním případě ovšem máme jistotu, co soubor obsahuje. Ve druhém případě je to pravděpodobně náhodný pozůstatek předchozích dat. Rychlost provedení je ovšem velmi rozdílná.
12 V právě vytvořeném (či otevřeném) souboru se nacházíme na jeho počátku.
13 Takový přístup můžeme provést například tehdy, trvá-li výpočet hodnot příliš dlouho. Pak je výhodnější hodnoty jedenkrát vypočíst a uložit do souboru.. Dále budeme data pouze číst.
14 Vždyť jistě stačí char *s a dále jen s = "Uz to umim!\n". Tedy bez nutnosti kopírování. Pouze nastavíme ukazatel s na počátek řetězce. Navíc bychom ušetřili i místo pro vyrovnávací paměť.
15 Chceme-li být velmi opatrní, zvolíme jako maximální délku řádku 512 znaků. Mnoho unixovských programů má právě toto omezení.
16 Nesmíme zapomenout soubor.txt nejprve předchozím spuštěním dříve uvedeného programu vytvořit.
17 Jeho definici můžeme nalézt i v kapitole Odvozené a strukturované typy dat.
18 Při startu programu je jako default hodnota aktivní režim textový. Tento stav je ovšem možno změnit nastavením hodnot O_TEXT, případně O_BINARY v globální proměnné _fmode (je deklarována v stdlib.h a v fcntl.h (zde se rovněž nacházejí O_... konstanty).
19 Máme-li dostatečně velikou vyrovnávací paměť.
20 Tím spíše, když napíšeme pouze operátor násobení mezi operandy. Práci za nás vykoná stroj.


Předchozí kapitola

Obsah

Začátek

Následující kapitola


Název: Programování v jazyce C
Autor: Petr Šaloun
Do HTML převedl: Kristian Wiglasz
Poslední úprava: 19.11.1996