Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 aktuálnímu pracovnímu adresáři (např. aktuální adresář shellu, když program spouštíme). 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
'b' otevreno v binarnim rezimu

PRIKLADY KOMBINACI
"r" cteni v textovem rezimu
"w" zapis v textovem rezimu 
"rb" cteni v binarnim rezimu 
"wb" zapis v binarnim rezimu
*/

Pokud se v řetězci nenachází znak b, otevře se proud v textovém režimu. Některé verze knihovny tolerují i t jako příznak otevření v textovém režimu, ovšem ve standardu není.

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", "rt");
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

// vraci true, pokud byl neuspech predchozi funkce zpusoben tim, 
// ze jsme na konci souboru 
int feof(FILE* stream); 

// vraci true, pokud byl neuspech predchozi funkce zpusoben
// jinou chybou.
int ferror(FILE* stream); 

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, jako hodnoty int získanou přetypováním z unsigned char. (Dolní bajt je naše hodnota). Při neúspěšném čtení vrací konstantu EOF, která má zápornou hodnotu (typicky -1). Pokud dojde k úspěšnému čtení, návratová hodnota getc je nezáporná. Srovnáním návratové hodnoty s EOF tak vždycky správně zjistíme, jesli došlo k chybě. Toto srovnání ovšem musíme provést předtím, než návratovou hodnotu přetypujeme na char, např.

int z;
while ((z = getc(in)) != EOF)
{
	char c = (char)z;   // pretypovani az po testu
	fputc(z, stdout);
}

Problém může nastat v následujícím příkladu.

char c;
while ((c = getc(in)) != EOF) { ... }
//
// pokud nacteme bajt 0xFF, ulozi se jako -1, tedy jako EOF
// cyklus skonci uprostred souboru
//

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

  1. Napište program, který spočítá počet znaků, řádků a slov v textovém souboru. (Analogie textové utility wc).

  2. 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).

  3. Nastudujte v referenční příručce a vyzkoušejte funkci fgets. Naprogramujte funkci, která pomocí fgets načte řádek do bufferu pevné velikosti a pozná, že se celý řádek nevešel.

  4. 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ů.

  5. 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í nekomentovaný řádek obsahuje řetězec "P1".
    • Druhý nekomentovaný řá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 nekomentovaný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);
    }
    
  6. 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
    --------------------------------------------------------------------------
    
  7. 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 aaabaabcccccbccccd zakódujeme na

    (a,3)(b,1)(a,2)(b,1)(c,5)(b,1)(c,4)(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), kde x je symbol a n je počet jeho výskytů budeme do výsledného souboru ukládat pomocí dvou bajtů: do prvního uložíme x, do druhého uložíme n-1. Takto jsme schopni uložit pouze dvojice, kde je n <= 256. Dvojice, kde je n>256, si představíme jako posloupnost n/256 dvojic (x, 256) následovanou dvojicí (x, r), kde r = n % 256. Tuto poslední dvojici vynecháme, pokud je r == 0. Tyto dvojice poté uložíme stejným způsobem, jako dvojice s n<=256.

    Kompresní program implementujte jako funkce

    void RLE_encode(char* in_path, char *out_path);
    void RLE_decode(char* in_path, char *out_path);
    
  8. Napište program, který čte ze standardního vstupu a zapisuje na standardní výstup, přičemž převede všechna malá písmena na velká. Program nesmí otevírat žádný soubor. Vyzkoušejte ho přesměrováním:

    ./program < vstup.txt > vystup.txt
    echo "ahoj svete" | ./program
    

    Chybová hlášení tiskněte na stderr a ověřte, že se do vystup.txt nedostanou.

  9. Napište funkci void copy_file(char *src, char *dst), která zkopíruje soubor v binárním režimu po blocích o velikosti 4096 bajtů pomocí fread a fwrite. Ošetřete poslední, neúplný blok. Porovnejte rychlost s variantou, která kopíruje po jednotlivých bajtech pomocí getc a fputc.

  10. Napište funkci long find_byte(char *filename, unsigned char b), která vrátí offset prvního výskytu bajtu b v souboru, nebo -1. Napište i variantu, která hledá od konce souboru — tam využijte fseek se SEEK_END a ftell.

  11. Soubor obsahuje na každém řádku čísla oddělená středníkem. Napište funkci, které se předá jméno souboru a číslo sloupce, a která vytiskne na stdout součet hodnot v tomto sloupci. Řádky, ve kterých sloupec chybí, přeskočte a jejich počet vypište na stderr.

Řešení úkolů

Úkol 1

Napište program, který spočítá počet znaků, řádků a slov v textovém souboru (analogie wc).

void wc(char *filename)
{
	FILE *in = fopen(filename, "r");
	assert(in);

	long chars = 0, lines = 0, words = 0;
	int  in_word = 0;
	int  z;

	while ((z = getc(in)) != EOF)
	{
		chars += 1;
		if (z == '\n') { lines += 1; }

		if (z == ' ' || z == '\t' || z == '\n') { in_word = 0; }
		else if (!in_word) { in_word = 1; words += 1; }   // zacatek noveho slova
	}

	fclose(in);
	printf("%ld %ld %ld %s\n", lines, words, chars, filename);
}

Slova počítáme pomocí příznaku in_word: nové slovo začíná tam, kde po bílém znaku přijde znak, který bílý není. Bez příznaku bychom počítali mezery a dvě mezery za sebou by daly špatný výsledek.

Návratová hodnota getc musí být v int, ne v char — jinak by se bajt 0xFF stal nerozlišitelným od EOF.

Řádky počítáme podle znaků nového řádku, takže poslední řádek bez ukončujícího \n se nezapočítá. Skutečné wc se chová stejně.

Úkol 2

Napište funkci, která z proudu načte jeden řádek, když předem neznáme délky řádků. Zajistěte, aby šla používat opakovaně.

//
// vraci nove alokovany retezec bez znaku noveho radku,
// nebo 0, pokud uz nebylo co cist
// pamet uvolnuje volajici
//
char *read_line(FILE *in)
{
	size_t cap = 16;
	size_t len = 0;
	char  *buf = malloc(cap);
	assert(buf);

	int z;
	while ((z = getc(in)) != EOF && z != '\n')
	{
		if (len + 1 >= cap)          // +1 kvuli ukoncovaci nule
		{
			cap *= 2;
			char *p = realloc(buf, cap);
			assert(p);
			buf = p;
		}
		buf[len] = (char)z;
		len += 1;
	}

	if (z == EOF && len == 0) { free(buf); return 0; }   // konec souboru

	buf[len] = 0;
	return buf;
}

Použití:

char *line;
while ((line = read_line(in)))
{
	printf("[%s]\n", line);
	free(line);
}

Návrh rozhraní je tu podstatnější než kód. Funkce vrací nově alokovanou paměť, takže musí být jasně řečeno, že ji uvolňuje volající. Alternativou by bylo předat buffer a jeho velikost odkazem a nechat funkci, aby si ho v případě potřeby zvětšila — to je přesně to, co dělá POSIXová getline.

Kapacitu zdvojnásobujeme, takže je celkový počet realokací logaritmický v délce řádku.

Rozlišení „prázdný řádek“ versus „konec souboru“ je v podmínce z == EOF && len == 0. Prázdný řádek vrátí prázdný řetězec, konec souboru vrátí 0.

Úkol 3

Nastudujte v referenční příručce a vyzkoušejte funkci fgets.

char *fgets(char *str, int count, FILE *stream);

Načte nejvýše count - 1 znaků do str a ukončí ho nulou. Čtení skončí na znaku nového řádku (ten se do bufferu uloží) nebo na konci souboru. Vrací str, nebo 0, pokud se nenačetlo nic.

char buf[64];
while (fgets(buf, sizeof(buf), in))
{
	printf("[%s]", buf);
}

Dvě věci, které je potřeba vědět:

  • Znak \n v bufferu zůstává. Chceme-li ho odstranit, hledá se ručně.
  • Nevešel-li se řádek celý, fgets skončí bez \n a zbytek řádku zůstává v proudu. Rozlišit obojí lze právě podle přítomnosti \n.
//
// vraci 1, pokud se cely radek vesel
//
int read_whole_line(FILE *in, char *buf, int size)
{
	if (!fgets(buf, size, in)) { return 0; }

	size_t len = strlen(buf);
	if (len > 0 && buf[len-1] == '\n') { buf[len-1] = 0; return 1; }
	return 0;    // radek byl delsi nez buffer
}

Funkce gets (bez f) dělala totéž bez omezení velikosti, a proto z jazyka byla v C11 odstraněna. Nikdy ji nepoužívejte.

Úkol 4

Napište program, který uloží kopii textového souboru tak, že jeden řádek bude mít maximálně 80 znaků, nerozdělí žádné slovo kratší než 80 znaků a minimalizuje počet řádků.

void wrap_file(char *src, char *dst, int width)
{
	FILE *in  = fopen(src, "r"); assert(in);
	FILE *out = fopen(dst, "w"); assert(out);

	int  col = 0;           // kolik znaku uz je na aktualnim radku
	char word[1024];
	int  wl = 0;            // delka rozepsaneho slova
	int  z;

	while (1)
	{
		z = getc(in);
		int is_sep = (z == EOF || z == ' ' || z == '\n' || z == '\t');

		if (!is_sep)
		{
			if (wl < 1023) { word[wl] = (char)z; wl += 1; }
			continue;
		}

		if (wl > 0)
		{
			//
			// vejde se slovo i s oddelujici mezerou na aktualni radek?
			//
			if (col > 0 && col + 1 + wl > width) { fputc('\n', out); col = 0; }
			else if (col > 0) { fputc(' ', out); col += 1; }

			fwrite(word, 1, (size_t)wl, out);
			col += wl;
			wl = 0;
		}

		if (z == EOF) { break; }
	}

	if (col > 0) { fputc('\n', out); }
	fclose(in);
	fclose(out);
}

Algoritmus je hladový: slovo dáme na aktuální řádek, pokud se tam i s oddělující mezerou vejde, jinak začneme nový. Tím je počet řádků minimální.

Klíčová je podmínka col + 1 + wl > width: ta jednička je mezera, kterou bychom museli před slovo napsat. Na začátku řádku (col == 0) se mezera nepíše.

Původní rozdělení na řádky se ztrácí, což zadání dovoluje. Chtěli-li bychom zachovat odstavce, museli bychom si pamatovat prázdné řádky.

Řešení výše je omezeno na slova délky nejvýše 1024 znaků (delší slova rozdělí). Abychom dostali verzi, ve které je délka slova neomezena, musíme nahradit pole word dynamickým polem.

Úkol 5

Načtení, zrcadlení a uložení obrázku ve formátu PBM.

Formát načteme jako proud položek, nikoliv po řádcích. Ušetříme si tím několik starostí: nepotřebujeme buffer pro ukládání řádků i starost o to, kde přesně řádky končí. Navíc získáme řešení, které zvládne i soubory, jejichž řádkování zadání nepředpokládá.

typedef struct
{
	int            width;
	int            height;
	unsigned char *data;    // radky za sebou, jeden bajt na pixel
} Picture;

První vrstva: znak po znaku, komentáře neviditelné

//
// vrati dalsi znak souboru, komentare (# az do konce radku) preskoci
//
static int pbm_next_char(FILE *in)
{
	int z = getc(in);
 
	while (z == '#')
	{
		while ((z = getc(in)) != EOF && z != '\n') { }   // zbytek radku zahodime
		if (z != EOF) { z = getc(in); }                  // a vezmeme dalsi znak
	}
	return z;
}
 
static int pbm_is_space(int z)
{
	return z == ' ' || z == '\t' || z == '\n' || z == '\r' || z == '\v' || z == '\f';
}
 
//
// prvni znak, ktery neni bily ani soucasti komentare
//
static int pbm_skip_space(FILE *in)
{
	int z;
	do { z = pbm_next_char(in); } while (pbm_is_space(z));
	return z;
}

Jakmile čteme pomocí pbm_next_char, komentáře pro zbytek programu prostě neexistují — nikde jinde už se na # nemusíme ptát.

Druhá vrstva: čtení jednotlivých položek

static void pbm_expect_magic(FILE *in)
{
	int a = pbm_skip_space(in);
	int b = pbm_next_char(in);       // mezi P a 1 mezera byt nesmi
	assert(a == 'P' && b == '1');
}
 
static int pbm_read_int(FILE *in)
{
	int z = pbm_skip_space(in);
	assert(z >= '0' && z <= '9');
 
	int value = 0;
	while (z >= '0' && z <= '9')
	{
		assert(value <= (2000000000 - 9) / 10);   // at nepreteceme
		value = value * 10 + (z - '0');
		z = pbm_next_char(in);
	}
 
	//
	// z uz k cislu nepatri, vratime ho do proudu pro dalsi cteni
	//
	if (z != EOF) { ungetc(z, in); }
	return value;
}
 
static int pbm_read_pixel(FILE *in)
{
	int z = pbm_skip_space(in);
	assert(z == '0' || z == '1');
	return z - '0';
}

Funkce ungetc vrátí jeden znak zpět do proudu, takže ho příští čtení dostane znovu. Norma zaručuje, že jeden znak vrátit lze vždy; na víc se spoléhat nelze. Je to standardní způsob, jak si při ručním rozboru poradit s tím, že konec čísla poznáme až podle znaku, který za ním následuje.

Kontrola přetečení v pbm_read_int není zbytečná: rozměry čteme ze souboru, tedy z dat, kterým nemůžeme věřit. Bez ní by stačil soubor s dostatečně dlouhým číslem a šlo by o nedefinované chování.

Třetí vrstva: obrázek

Picture load_from_pbm(char *filename)
{
	FILE *in = fopen(filename, "r");
	assert(in);
 
	Picture p;
	pbm_expect_magic(in);
	p.width  = pbm_read_int(in);
	p.height = pbm_read_int(in);
	assert(p.width > 0 && p.height > 0);
 
	p.data = malloc((size_t)p.width * (size_t)p.height);
	assert(p.data);
 
	//
	// pixely ctame jako souvislou posloupnost, na radkovani nezalezi
	//
	for (int i = 0; i < p.width * p.height; i += 1)
	{
		p.data[i] = (unsigned char)pbm_read_pixel(in);
	}
 
	fclose(in);
	return p;
}

Data držíme v jednorozměrném poli a indexujeme y * width + x, stejně jako u matice v kapitole Paměť. Protože se pixely čtou jako jeden dlouhý proud, je úplně jedno, jak jsou v souboru rozdělené do řádků. Všimněte si, že assert v pbm_read_pixel odhalí i soubor, ve kterém je pixelů méně, než slibuje hlavička: čtení narazí na EOF, který číslicí není.

void mirror_picture(Picture *p)
{
	for (int y = 0; y < p->height; y += 1)
	{
		for (int x = 0; x < p->width / 2; x += 1)
		{
			unsigned char t = p->data[y*p->width + x];
			p->data[y*p->width + x] = p->data[y*p->width + p->width-1-x];
			p->data[y*p->width + p->width-1-x] = t;
		}
	}
}
 
void save_to_pbm(char *filename, Picture src)
{
	FILE *out = fopen(filename, "w");
	assert(out);
 
	fprintf(out, "P1\n%i %i\n", src.width, src.height);
 
	for (int y = 0; y < src.height; y += 1)
	{
		for (int x = 0; x < src.width; x += 1)
		{
			fprintf(out, "%i", src.data[y*src.width + x]);
			if (x + 1 < src.width) { fputc(' ', out); }
		}
		fputc('\n', out);
	}
	fclose(out);
}
 
void destroy_picture(Picture *p)
{
	free(p->data);
	p->data = 0;
	p->width = 0;
	p->height = 0;
}

Zrcadlení běží jenom do poloviny řádku — kdybychom šli přes celý, prohodili bychom každou dvojici dvakrát a obrázek by zůstal beze změny.

Úkol 6

Binární formát pro ukládání řetězců: funkce append a search.

void append(char *string, char *filename)
{
	FILE *out = fopen(filename, "ab");     // "a" pripoji na konec
	assert(out);

	size_t len = strlen(string);
	assert(len <= 65535);                  // do dvou bajtu se vic nevejde

	//
	// delku zapisujeme po bajtech, abychom nezavisel na endianite
	// (little endian: nizsi bajt prvni)
	//
	unsigned char hdr[2];
	hdr[0] = (unsigned char)(len & 0xFF);
	hdr[1] = (unsigned char)((len >> 8) & 0xFF);

	fwrite(hdr, 1, 2, out);
	fwrite(string, 1, len, out);
	fclose(out);
}

long search(char *string, char *filename)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	size_t target = strlen(string);
	long   offset = 0;

	unsigned char hdr[2];
	char          buf[65536];

	while (fread(hdr, 1, 2, in) == 2)
	{
		size_t len = (size_t)hdr[0] | ((size_t)hdr[1] << 8);

		if (fread(buf, 1, len, in) != len) { break; }   // poskozeny soubor

		if (len == target && memcmp(buf, string, len) == 0)
		{
			fclose(in);
			return offset;
		}
		offset += 2 + (long)len;
	}

	fclose(in);
	return -1;
}

Délku skládáme z jednotlivých bajtů ručně místo fwrite(&len, sizeof(len), 1, out). Kdybychom zapsali size_t přímo, závisel by formát souboru na velikosti typu i na endianitě dané platformy — a soubor by nešel přenést jinam.

Porovnáváme nejdřív délky a teprve potom obsah. Tím se ušetří většina volání memcmp a zároveň se ověří, že porovnáváme stejně dlouhé bloky.

Soubor je nutné otevřít v binárním režimu ("rb", "ab"). V textovém režimu by se na Windows bajt 0x0A převáděl na dvojici a offsety by neseděly.

Úkol 7

Naprogramujte funkce RLE_encode a RLE_decode.

void RLE_encode(char *in_path, char *out_path)
{
	FILE *in  = fopen(in_path, "rb");  assert(in);
	FILE *out = fopen(out_path, "wb"); assert(out);

	int cur = getc(in);
	while (cur != EOF)
	{
		//
		// spocitame delku behu stejnych bajtu
		//
		long n = 1;
		int  next;
		while ((next = getc(in)) == cur) { n += 1; }

		//
		// do jedne dvojice se vejde nejvyse 256 opakovani,
		// do druheho bajtu ukladame n-1
		//
		while (n >= 256)
		{
			fputc(cur, out);
			fputc(255, out);
			n -= 256;
		}
		if (n > 0)
		{
			fputc(cur, out);
			fputc((int)(n - 1), out);
		}

		cur = next;
	}
	fclose(in);
	fclose(out);
}

void RLE_decode(char *in_path, char *out_path)
{
	FILE *in  = fopen(in_path, "rb");  assert(in);
	FILE *out = fopen(out_path, "wb"); assert(out);

	int x;
	while ((x = getc(in)) != EOF)
	{
		int c = getc(in);
		assert(c != EOF);            // liche mnozstvi bajtu = poskozeny soubor

		for (int i = 0; i <= c; i += 1) { fputc(x, out); }
	}
	fclose(in);
	fclose(out);
}

Konstrukce while (n >= 256) řeší okrajový případ ze zadání elegantněji než počítání n / 256 a n % 256: pro n dělitelné 256 se prostě poslední dvojice nezapíše, protože po odečtení zbyde nula. Právě to je ta výjimka „poslední dvojici vynecháme, pokud je r == 0“ — kdybychom zapsali (x, 0), uložili bychom do druhého bajtu hodnotu −1.

Dekódování je jednodušší: čteme po dvojicích a c + 1 krát vypíšeme bajt. Cyklus proto běží i <= c, nikoliv i < c.

Všimněte si, že RLE může data i zvětšit — soubor bez opakování bude po zakódování dvakrát delší. Skutečné kompresní formáty proto kombinují RLE s dalšími technikami nebo si u každého bloku pamatují, zda se vyplatilo ho komprimovat.

Úkol 8

Program, který čte ze standardního vstupu a zapisuje na standardní výstup, přičemž převede malá písmena na velká.

#include <stdio.h>

int main()
{
	int z;
	while ((z = getc(stdin)) != EOF)
	{
		if (z >= 'a' && z <= 'z') { z = z - 'a' + 'A'; }

		if (fputc(z, stdout) == EOF)
		{
			fprintf(stderr, "chyba zapisu\n");
			return 1;
		}
	}
	return 0;
}

Návratová hodnota getc je opět v int, nikoliv v char — jinak by bajt 0xFF nešel odlišit od EOF.

Úkol 9

Funkce copy_file, která kopíruje soubor po blocích o velikosti 4096 bajtů.

void copy_file(char *src, char *dst)
{
	FILE *in  = fopen(src, "rb"); assert(in);
	FILE *out = fopen(dst, "wb"); assert(out);

	unsigned char buf[4096];
	size_t n;

	//
	// fread vraci pocet SKUTECNE prectenych prvku,
	// posledni blok tak vyjde kratsi sam od sebe
	//
	while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
	{
		size_t written = fwrite(buf, 1, n, out);
		assert(written == n);
	}

	assert(!ferror(in));    // cyklus mohl skoncit i chybou, ne jen koncem souboru
	fclose(in);
	fclose(out);
}

Neúplný poslední blok se řeší sám: velikost prvku je 1 bajt a počet prvků 4096, takže fread vrátí kolik zbylo, a přesně tolik zapíšeme. Kdybychom to obrátili (velikost prvku 4096, počet 1), vrátila by funkce na konci nulu a poslední neúplný blok bychom ztratili.

Cyklus končí i při chybě čtení, proto je za ním ferror. Bez té kontroly by se poškozený soubor tvářil jako úspěšně zkopírovaný.

Soubor je nutné otevřít v binárním režimu. V textovém by se na Windows převáděly konce řádků a kopie by nebyla přesná.

Úkol 10

Funkce find_byte hledající offset prvního výskytu bajtu, a varianta hledající od konce.

long find_byte(char *filename, unsigned char b)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	long offset = 0;
	int  z;

	while ((z = getc(in)) != EOF)
	{
		if ((unsigned char)z == b) { fclose(in); return offset; }
		offset += 1;
	}

	fclose(in);
	return -1;
}

Hledání od konce se neobejde bez fseek a ftell:

long find_byte_last(char *filename, unsigned char b)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
	long pos = ftell(in);           // velikost souboru v bajtech

	while (pos > 0)
	{
		pos -= 1;
		fseek(in, pos, SEEK_SET);
		int z = getc(in);
		if (z != EOF && (unsigned char)z == b) { fclose(in); return pos; }
	}

	fclose(in);
	return -1;
}

Kombinace fseek(SEEK_END) a ftell je standardní způsob, jak zjistit velikost souboru. Funguje spolehlivě jenom v binárním režimu — v textovém je hodnota z ftell neprůhledná a nemusí odpovídat počtu bajtů.

Pozor na pořadí v cyklu: nejdřív pos -= 1, potom čtení. Po SEEK_END totiž pos ukazuje za poslední bajt, kde už žádný není.

Porovnání (unsigned char)z == b je podstatné. getc vrací hodnotu v rozsahu 0 až 255, ale kdybychom si ji uložili do char, na platformě se znaménkovým char by se z hodnot nad 127 staly záporné a porovnání by selhalo.

Hledání od konce je výrazně pomalejší — pro každý bajt se volá fseek. U velkého souboru by se vyplatilo číst bloky odzadu a prohledávat je v paměti.

Úkol 11

Funkce, které se předá jméno souboru a číslo sloupce a která vypíše součet hodnot v tomto sloupci; řádky bez daného sloupce přeskočí a jejich počet vypíše na stderr.

void sum_column(char *filename, int column)
{
	FILE *in = fopen(filename, "r");
	assert(in);

	char   line[4096];
	double total   = 0;
	long   skipped = 0;

	while (fgets(line, sizeof(line), in))
	{
		int   col   = 0;
		char *start = line;
		int   found = 0;

		for (char *p = line; ; p += 1)
		{
			if (*p == ';' || *p == '\n' || *p == 0)
			{
				if (col == column)
				{
					//
					// docasne ukoncime pole nulou, aby strtod vedelo, kde skoncit
					//
					char save = *p;
					*p = 0;

					char  *end;
					double v = strtod(start, &end);
					if (end != start) { total += v; found = 1; }

					*p = save;
				}
				col   += 1;
				start  = p + 1;
				if (*p == '\n' || *p == 0) { break; }
			}
		}

		if (!found) { skipped += 1; }
	}

	fclose(in);

	printf("soucet sloupce %i: %f\n", column, total);
	fprintf(stderr, "preskocenych radku: %ld\n", skipped);
}

Funkce strtod ze stdlib.h převede začátek řetězce na double a přes parametr end řekne, kam až se dostala. Je-li end rovno start, nepřevedla se ani jedna číslice, což poznáme jako chybějící nebo nečíselnou hodnotu. Tím je ošetřen i řádek x;y.

Trik s dočasným zápisem nuly a jejím obnovením je běžný postup, jak knihovní funkci podstrčit část řetězce bez kopírování. Používat ho lze jen na zapisovatelný buffer — u řetězcového literálu by šlo o nedefinované chování.

Kdybychom se chtěli vyhnout bufferu fixní velikosti a použití knihovní funkce strtod, můžeme tak učinit následovně:

Řádek si nikam neukládáme. Čteme znak po znaku a číslo skládáme průběžně, jak znaky přicházejí — díky tomu nepotřebujeme ani buffer, ani žádnou funkci na rozbor textu. Vystačíme si s stdio.h a assert.h.

Inkrementální rozbor čísla

Stav rozepsaného čísla si držíme ve struktuře. Číslo se skládá ze znaménka, číslic před tečkou a číslic za ní; navíc si pamatujeme, zda dosud nepřišlo něco, co do čísla nepatří.

typedef struct
{
	int    valid;      // zatim nic neodporuje zapisu cisla
	int    has_digit;
	int    sign_seen;
	int    neg;
	int    after_dot;
	int    decimals;   // pocet cislic za teckou
	int    ended;      // uz jsme za cislem (nasledovala mezera)
	double value;      // vsechny cislice jako jedno cele cislo
} Number;
 
static void number_reset(Number *n)
{
	n->valid     = 1; 
	n->has_digit = 0; 
	n->sign_seen = 0; 
	n->neg       = 0;
	n->after_dot = 0; 
	n->decimals  = 0; 
	n->ended     = 0; 
	n->value     = 0.0;
}

Jádrem je funkce, které se předá jeden znak a která podle něj stav posune. Jakmile jednou narazí na něco nečekaného, nastaví valid na 0 a dál už jen mlčky vrací.

static void number_feed(Number *n, int c)
{
	if (!n->valid) { return; }
 
	if (c == ' ' || c == '\t' || c == '\r')
	{
		if (n->has_digit) { n->ended = 1; }              // mezera ZA cislem
		else if (n->sign_seen || n->after_dot) { n->valid = 0; }
		return;                                          // jinak mezera PRED cislem
	}
 
	if (n->ended) { n->valid = 0; return; }              // neco az za bílým znakem
 
	if (c == '+' || c == '-')
	{
		if (n->sign_seen || n->has_digit || n->after_dot) { n->valid = 0; return; }
		n->sign_seen = 1;
		n->neg = (c == '-');
		return;
	}
 
	if (c == '.')
	{
		if (n->after_dot) { n->valid = 0; return; }      // druha tecka
		n->after_dot = 1;
		return;
	}
 
	if (c >= '0' && c <= '9')
	{
		n->has_digit = 1;
		n->value = n->value * 10.0 + (c - '0');
		if (n->after_dot) { n->decimals += 1; }
		return;
	}
 
	n->valid = 0;                                        // cokoliv jineho
}

Všimněte si, jak se zachází s desetinnou částí. Číslice nepřičítáme rovnou jako zlomky, ale skládáme je všechny do jednoho celého čísla a počítáme si, kolik jich bylo za tečkou. Teprve na konci vydělíme příslušnou mocninou deseti.

//
// vraci 1, pokud je n platnym cislem; vysledek zapise do out
//
static int number_finish(Number *n, double *out)
{
	if (!n->valid || !n->has_digit) { return 0; }
 
	double p = 1.0;
	for (int i = 0; i < n->decimals; i += 1) { p *= 10.0; }
 
	*out = (n->neg ? -n->value : n->value) / p;
	return 1;
}

Podmínka !n->has_digit odmítne prázdné n i samotné znaménko nebo tečku. Mocninu deseti počítáme cyklem, takže nepotřebujeme pow ani math.h.

Průchod souborem

void sum_column(char *filename, int column)
{
	FILE *in = fopen(filename, "r");
	assert(in);
 
	double total   = 0.0;
	long   skipped = 0;
 
	int    col   = 0;
	int    found = 0;    // mel tento radek platnou hodnotu v hledanem sloupci?
	int    any   = 0;    // obsahuje radek vubec neco?
	Number num;
	number_reset(&num);
 
	while (1)
	{
		int z = getc(in);
 
		if (z != '\n' && z != EOF) { any = 1; }
 
		if (z == ';' || z == '\n' || z == EOF)
		{
			//
			// konec pole v souboru: je-li to ten hledany sloupec, zkusime ho pouzit
			//
			if (col == column)
			{
				double v;
				if (number_finish(&num, &v)) { total += v; found = 1; }
			}
			col += 1;
			number_reset(&num);
 
			//
			// konec radku
			//
			if (z == '\n' || z == EOF)
			{
				if (any && !found) { skipped += 1; }
				col = 0; found = 0; any = 0;
				if (z == EOF) { break; }
			}
			continue;
		}
 
		number_feed(&num, z);
	}
 
	fclose(in);
 
	printf("soucet sloupce %i: %f\n", column, total);
	fprintf(stderr, "preskocenych radku: %ld\n", skipped);
}

Celý program je jeden průchod souborem, ve kterém se nikdy nedrží víc než jeden znak. Paměťová náročnost je proto konstantní bez ohledu na délku řádku.

Konec pole a konec řádku se zpracovávají na témže místě, protože oba znamenají „právě dočtené pole je hotové“. Rozdíl je jen v tom, že konec řádku navíc vyhodnotí celý řádek a vynuluje čítač sloupců. Díky tomu funguje i soubor, který posledním řádkem nekončí znakem nového řádku — větev pro EOF je tatáž jako pro '\n'.

Příznak any odliší skutečný řádek od prázdného posledního řádku, který by se jinak započítal jako přeskočený.

Rozbor nepodporuje exponenciální zápis (1e5) ani jiný desetinný oddělovač než tečku. Obojí by šlo doplnit rozšířením number_feed o další stavy.

Velmi dlouhá čísla přesnost typu double postupně vyčerpají, stejně jako u kterékoliv jiné varianty. Na rozdíl od strtod tu ovšem nedochází k nedefinovanému chování, jen k zaokrouhlení.