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.
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
.
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; }
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; }
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
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.
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.
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...
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 FILE
17. 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.
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
.
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
.
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
.
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, ...]);
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.
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.
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.
Název: | Programování v jazyce C |
Autor: | Petr Šaloun |
Do HTML převedl: | Kristian Wiglasz |
Poslední úprava: | 19.11.1996 |