Proud je prostředek, kterým lze s pomocí standardní knihovny pracovat se soubory a dalšími vstupně-výstupními zařízeními (např. klávesnice, terminál). Existují i další prostředky, například systémová volání nebo jiné, nestandardní, knihovny. Těm se ovšem věnovat nebudeme.
Funkce, o kterých budeme mluvit, mohou skončit chybou. Uvedeme si jenom,
jakým způsobem poznáme, že k chybě došlo a proč k ní mohlo dojít.
Zpracování chyby a to, jak se má program chovat, pokud k chybě dojde,
je mimo rozsah tohoto kurzu. V příkladech budeme možnost, že k chybě
dojde buď ignorovat, nebo použijeme assert.
Proudy ze standardní knihovny
Proud je objekt, do kterého lze zapisovat nebo z něj číst. Otevřením
souboru jej na proud napojíme a poté zapisování do či čtení z proudu
odpovídá zapisování do souboru či čtení ze souboru. Jsou definovány i
další, tzv. standardní proudy. Pomocí nich můžeme zapisovat na
standardní výstup (tam zapisuje např.printf), na standardní chybový
výstup a číst ze standardního vstupu.
Proud je reprezentován strukturou FILE, v programu pracujeme s
ukazateli na tuto strukturu. Získáme jej pomocí funkce fopen.
FILE* fopen(char* path, char* mode);
První argument je cesta k otevíranému souboru (ta je buď absolutní,
nebo relativní vzhledem k umístění binárky spoušteného
programu). Textový řetězec mode určuje způsob otevření
souboru. Jeden ze znaků mode
obvykle udává režim otevření: ten je
buď textový nebo binární. Další znak pak to, co se souborem můžeme
dělat: číst jej, zapisovat do něj, připojit něco na jeho konec,
vytvořit jej, pokud neexistuje, nebo nějaká kombinace
předchozího. Nejpoužívanější možnosti ukážeme níže.
/*
POPIS ZNAKU
'r' otevreno pro cteni
'w' otevreno pro zapis, existujici soubor je prepsan
'a' otevreno pro zapis, pokud soubor existuje, zapisujeme na jeho konec
't' otevreno v textovem rezimu 'b' otevreno v binarnim rezimu
PRIKLADY KOMBINACI
"rt" cteni v textovem rezimu
"wt" zapis v textovem rezimu
"rb" cteni v binarnim rezimu
"wb" zapis v binarnim rezimu
*/
Pokud se v řetězci nenachází ani jeden ze znaků
tb, otevře se proud v textovém režimu.
Pokud otevření souboru selže, vrátí fopen 0.
Tuto situaci je potřeba ošetřit, pro účely kurzu
se spokojíme s assercí. (Pro zjištění důvodu
lze někdy použít kombinaci errno a strerror.)
FILE *st = fopen("path/to/file", "t");
assert(st);
Nepoužívaný proud se zavírá funkcí fclose.
V textovém režimu čteme a zapisujeme znaky, v binárním
režimu bajty. Ačkoliv se zdá, že je to totéž, rozdíl je
například u konce řádku ve Windows, který končí dvojící
bajtů '\r' '\n'.
Ze streamu se čte a zapisuje se do něj pomocí
funkcí z stdio.h. Stream si udržuje místo, ze kterého
se bude číst při příštím zavolání funkce pro čtení.
Po přečtení nějakého úseku (např. znaku, řádku apod.)
posune toto místo na první bajt nepřečteného obsahu.
Pokud se knihovní funkci pro čtení ze streamu čtení nepovede, programátor to pozná z návratové hodnoty, případně může zkontrolovat příznaky streamu pro konec souboru a pro chybu. To se dělá funkcemi
int feof(FILE* stream); // vraci true, pokud jsme na konci souboru
int ferror(FILE* stream); // vraci true, pokud došlo k chybě
Analogické úvahy platí i pro zápis.
Textový režim
Základní funkce pro čtení je
int getc(FILE *stream);
Přečte ze streamu jeden znak, který získáme přetypováním
návratové hodnoty na char. Při neúspěšném čtení vrací
konstantu EOF.
Větší část textu lze načíst pomocí funkce fgets
(tu si čtenář dostuduje z referenční příručky).
Základními funkcemi pro zápis jsou
int fputc(int ch, FILE* stream);
int fputs(char *s, FILE* stream);
int fprintf(FILE* stream, char* format, ...),
Funkce fputc zapíše do streamu znak, který dostane
přetypováním ch na unsigned int. Funkce fputs
zapíše do streamu řetězec. V případě neúspěchu obě
funkce vracejí EOF a nastaví streamu flag ferror.
Funkce fprintf funguje jako printf pouze tiskne do streamu.
Při chybě vrací zápornou hodnotu, jinak počet vytistěných
znaků.
Zápis do streamu je bufferovaný, zapsané změny se v napojeném souboru (či zařízení) mohou projevit později. Chceme-li si jejich okamžité projevení, můžeme k tomu použít funkci
int fflush(FILE* stream);
V případě neúspěchu vrací EOF. Tato funkce je volána
(či je prováděn kód jí velmi podobný) i při zavření streamu,
proto fclose také může neuspět a vrátit EOF.
Existují tři standardní proudy, přistupné jsou
pomocí proměnných stdin, stdout, stderr.
Jako příklad si uvedeme funkci, která načte obsah souboru na disku a vytiskne jej na standardní výstup.
void print_file(char *filename)
{
FILE *in = fopen(filename, "rt");
assert(in);
int z = 0;
while (1)
{
z = getc(in);
if (z == EOF) // konec souboru nebo chyba cteni
{
if (ferror(in))
{
fprintf(stderr, "chyba cteni\n");
assert(0);
}
break;
}
fputc(z, stdout);
}
fclose(in);
}
Binární režim
Čteme a zapisujeme bajty, které při čtení nejsou nijak interpretovány.
Funkce pro čtení
size_t fread(void *dest, size_t element_size, size_t count, FILE *in)
Čteme count prvků, každý o velikost element_size bajtů, ze streamu in,
ukládáme je do pole na adrese dest. Vrací počet přečtených prvků, pokud
bylo čtení bez chyby, pak se count a návratová hodnota rovnají.
Je nutné myslet na to, že na adrese dest musí být naalokováno dostatek
paměti, jinak dojde k nedefinovanému chování.
Funkce pro zápis
size_t fwrite(void *src, size_t element_size, size_t count, FILE *out)
do streamu out zapise count položek, každy o velikosti element_size uložených
za sebou na adrese src. Funkce vrací počet zapsaných položek.
Můžeme také pracovat s akturální pozicí, ze které v souboru čteme. Pozice je
hodnota typu long a je to index bajtu od začátku. Aktuální pozici
můžeme získat pomocí funkce
long ftell(FILE* stream)
Aktuální pozici také můžeme nastavit pomocí funkce
int fseek(FILE *stream, long offset, int start)
kde offset je počet bajtů, o které se chceme posunout
a start určuje místo, ze kterého se chceme posunout.
Pro tento účel jsou definovány konstanty
SEEK_SET zacatek souboru
SEEK_CUR aktualni pozice
SEEK_END konec souboru
V následujícím příkladu zapíšeme obsah pole do souboru a zase je načteme.
char *filename = "tmp.xx";
// zapis do souboru
float out_array[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
FILE *out = fopen(filename, "wb");
assert(out);
size_t written = fwrite(out_array, sizeof(float), 4, out);
assert(written == 4);
fclose(out);
// cteni ze souboru, od druheho prvku
FILE *in = fopen(filename, "rb");
// posuneme se o jeden float dopredu
int fail = fseek(in, sizeof(float), SEEK_SET);
assert(!fail);
float in_array[3]= { 0.0f, 0.0f, 0.0f };
size_t read = fread(in_array, sizeof(float), 3, in);
assert(read == 3);
fclose(in);
for(int i = 0; i < 3; i += 1) {
printf("%f ", in_array[i]);
}
printf("\n");
Práce s binárními soubory a čtení binárních formátů má svá specifika, musíme si dávat pozor na velikosti typů, endianitu apod.
Úkoly
-
Napište program, který spočítá počet znaků, řádků a slov v textovém souboru. (Analogie textové utility
wc). -
Napište funkci, která z proudu načte jeden řádek, když předem neznáme délky řádků. Zajistěte, aby šla funkce používat opakovaně. (Musíte vymyslet vhodné argumenty, jejich předání a vypořádat se s alokacemi).
-
Nastudujte v referenční příručce a vyzkoušejte funkci
fgets. -
Napište program, který načte textový soubor a do jiného souboru uloží kopii jeho obsahu tak, že jeden řádek bude mít maximálně 80 znaků. Přitom nerozdělí žádná slova kratší než 80 znaků a minimalizuje počet řádků.
-
PBM je textový formát pro uložený černobílého obrázku (každý pixel je buď černý nebo bílý).
- Řádky začínající znakem
#jsou komentáře - První řádek obsahuje řetězec
"P1". - Druhý řádek obsahuje dvě čísla v desítkové soustavě oddělená mezerou, tato čísla udávají šiřku a výšku obrázku (v pixelech).
- Počínaje třetím řádkem obsahuje soubor pixelová data. Jeden pixel je buď 0 nebo 1. Pixely jsou zapsány za sebou po řádcích odshora obrázku, jeden řádek obrázky na jeden řádek souboru. Mezi pixely na jednom řádku je mezera.
Doplňte chybějící kód v následujícím. (Můžete si dopsat pomocné funkce atd.)
// // Pro jednoduchost případné chyby (nelze otevřít soubor atd.) // řešte pouze ukončením programu pomocí assert // // Volitelně lze dopsat funkci pro dealokaci vnitřních dat // struktury Picture (ale není to nutné, jde o procvičení práce // se streamy). // // // Struktura pro černobílý obrázek // typedef struct { // TODO } Picture; // // nahraje ze souboru obrázek ve formátu pbm // Picture load_from_pbm(char* filename) { // TODO } // // zrcadlově obrátí obrázek podle svislé osy // void mirror_picture(Picture* p) { // TODO } // // uloží obrázek v pbm formátu // void save_to_pbm(char* filename, Picture src) { // TODO } // // příklad použití // int main() { Picture picture = load_from_pbm("test.pbm"); mirror_picture(&picture); save_to_pbm("foo.pbm", picture); } - Řádky začínající znakem
-
Uvažme ukládání řetězců do souboru v binárním formátu.
Naprogramujte funkci
void append(char* string, char* filename), která na konec souboru přidá nový řetězec.Naprogramujte funkci
long search(char* string, char* filename), která prohledá soubor a pokud obsahuje záznam o daném řetězci, vrátí jeho offset od začátku souboru. Jinak vrátí -1.V souboru odpovídá řetězci jeden záznam, záznamy jsou v souboru za sebou. Jeden záznam vypadá následovně.
offset velikost vyznam -------------------------------------------------------------------------- 0 2 velikost retezce v bajtech 2 ? ulozeny retezec, chapan jako posloupnost unsigned char bez 0 na konci -------------------------------------------------------------------------- -
Run Length Encoding (RLE) je velmi jednoduchý bezeztrátový kompresní princip. Vysvětlíme jej na posloupnostech symbolů a později si řekneme, jak jej uplatníme v jednoduchém kompresním programu.
Vstupem RLE je posloupnost symbolů (symbol je zde abstraktní pojem, který nijak nevztahujeme k jazyku C, znakům v textu atd.). Tu převedeme do komprimované podoby s pomocí následujícího principu:
-
Pokud se v posloupnosti za sebou nachází n-krát symbol
x, převedeme ji na dvojici(x, n). -
Předchozí bod aplikujeme na maximálně dlouhé (= nejdou prodloužit) podposloupnosti vstupní posloupnosti v pořadí, ve kterém se ve vstupní posloupnosti vyskytují.
Například posloupnost
aaabaabcccccbccccdzakódujeme na(a,3)(b,1)(a,2)(b,1)(c,5)(b,1)(c,3)(d,1)Naprogramujte základ jednoduchého kompresního programu.
Soubor pro kompresi budeme otevírat v binárním módu. Pro RLE budeme za jeden symbol považovat jeden bajt souboru. Dvojici
(x, n), kdexje symbol anje počet jeho výskytů budeme do výsledného souboru ukládat pomocí dvou bajtů: do prvního uložímex, do druhého uložímen-1. Takto jsme schopni uložit pouze dvojice, kde jen <= 256. Dvojice, kde jen>256, si představíme jako posloupnostn/256dvojic(x, 256)následovanou dvojicí(x, r), kder = n % 256. Tyto dvojice poté uložíme stejným způsobem, jako dvojice sn<=256.Kompresní program implementujte jako funkce
void RLE_encode(char* in_path, char *out_path); void RLE_decode(char* in_path, char *out_path); -