Kapitola 10. Soubory

Obsah

10.1. Datové proudy
10.2. Základní datové proudy
10.3. Otevření a zavření proudu
10.4. Proud a vstup a výstup znaků
10.5. Proud a vstup a výstup řetězců
10.6. Proud a formátovaný vstup a výstup
10.7. Další užitečné funkce
10.8. Binární soubory
10.9. Opakování

Časová náročnost
Časová náročnost kapitoly: 1 hodina 20 minut

Soubor je posloupnost znaků (bajtů) ukončená nějakou speciální kombinací, která již k obsahu souboru nepatří - konec souboru, symbolicky EOF.

Důležité

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če.

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ému. Někdy se v této souvislosti zavádí pojmy vnější jméno souboru a vnitřní jméno souboru. Tím prvním rozumíme jméno souboru na úrovni OS, druhý pojem chápeme jako jednoznačnou identifikaci souboru v rámci programu v jazyce C - nejčastěji jde o jméno proměnné, jejímž prostřednictvím se souborem pracujeme.

Operační systém neví nic o obsahu souboru, nezná jeho typ souboru. Přípony jmen souborů jsou většinou pouze doporučeními. Binární soubor obvykle nemá smysl vypisovat na terminál. Textový soubor můžeme snadno zobrazit a číst.

ISO norma jazyka C nám umožňuje pracovat se soubory technikou datových proudů. Pro manipulaci s datovým proudem je k dispozici řada funkcí, které poskytují vysoký komfort. Navíc, díky normám, můžeme v dobré víře očekávat plnou přenositelnost našich zdrojových textů na všechny platformy.

Potřebné znalosti
Potřebné znalosti
K zvládnutí obsahu této kapitoly je nutné mít znalosti z kapitoly 2 až 9 - chápat a umět používat dříve probíranou teorii, rozumět problematice ukazatelů. Umět vytvářet pole (jedno i více rozměrná). Znát operace prováděné s řetězci a použití argumentů příkazového řádku.

10.1. Datové proudy

Časová náročnost
Časová náročnost: 6 minut

Než začneme probírat souborový I/O, musíme znát dva velice důležité pojmy: datový proud a soubor. O souboru jsme se již zmínili výše. Nyní se zaměříme na datový proud.

I/O systém jazyka C poskytuje programátorovi stále stejné rozhraní, bez ohledu na skutečně použité I/O zařízení. Aby se toho dalo dosáhnout, zavádí jazyk C mezi programátorem a zařízením (hardwarem) určitou úroveň abstrakce, která se nazývá datový proud (datový tok, stream). Skutečné zařízení provádějící I/O operace se nazývá soubor. Datový proud je tedy logickým rozhraním souboru. Za abstraktní soubor můžeme vzhledem k uvedenému považovat diskový soubor, obrazovku, klávesnici, port, tiskárnu a různé jiné. Nejčastějším typem je samozřejmě diskový soubor. Výhodou tohoto přístupu je, že pro programátora vypadá jedno hardwarové zařízení stejně jako druhé. Datový proud automaticky ošetřuje rozdíly.

Datový proud je připojen k souboru pomocí operace otevření (open) a odpojen od souboru pomocí operace uzavření (close).

Důležité ISO norma definuje dva režimy proudů - textový (rozlišuje řádky) a binární. Režim stanovíme při otevírání souboru.

Dalším důležitým pojmem je tzv. aktuální pozice. Je to místo v souboru, kde se bude provádět další operace se souborem. Například, je-li soubor dlouhý 100 byte a byla přečtena jeho polovina, pak bude další operace čtení probíhat od 50 bytu, což je aktuální pozice v souboru.

10.2. Základní datové proudy

Časová náročnost
Časová náročnost: 2 minuty

Základem pro přístup k proudu je datový typ FILE. Pro práci s datovými proudy musíme používat funkční prototypy umístěné v soubor stdio.h. Při každém spuštění programu máme otevřeny následující proudy:

Důležité
FILE *stdin;
FILE *stdout;
FILE *stderr;

10.3. Otevření a zavření proudu

Časová náročnost
Časová náročnost: 12 minut

Teprve po otevření můžeme s proudem pracovat. Při otevření proudu provádíme spojení mezi vnitřním a vnějším jménem souboru. Při otevření určujeme režim našeho přístupu k datům v proudu. Uzavřením proudu umožňujeme OS aktualizovat adresářové informace podle aktuálního stavu souboru, který byl s proudem spojen.

  • 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 operačního systému. Řetězec mode určuje režim práce se souborem i jeho typ.

  • int fclose(FILE *stream);

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

  • ferror()

    Informuje o chybách při práci s proudem.

  • perror()

    Pošle řetězec chybového hlášení do standardního chybového proudu, tj. do stderr.

Pozn:
Pozn:
Počet souborů, které můžeme z programu současně otevřít, je omezen operačním systémem (nejčastěji 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. Rovněž tuto hodnotu můžeme zjistit pomocí makra, tentokráte však FILENAME_MAX. Konec souboru představuje makro EOF.

Režimy práce s datovým proudem:

Důležité

Tabulka 10.1.

ŘetězecVýznam - otevření pro
rčtení
wzápis
apřipojení
r+aktualizace - update - jako rw
w+jako výše uvedené r+, ale existující proud ořízne na nulovou délku, jinak vytvoří nový
a+aktualizace, pokud neexistuje tak jej vytvoří
ttextový režim
bbinární režim
rbčtení v binárním režimu
wbzápis v binárním režimu
abpřipojení v binárním režimu

Důležité Ikdyž většina souborových režimů nevyžaduje bližší vysvětlení, je vhodné uvést několik poznámek. Pokud soubor otevíráme jen pro čtení a on neexistuje, funkce fopen() selže. Když se otevírá neexistující soubor s režimem přidávání, pak bude vytvořen. Otevírá-li se již existující soubor v režimu pro přidávání, budou nová data zapisovány automaticky na konec souboru. Ke změně existujících dat nedojde. Když se otevírá neexistující soubor pro zápis, pak se vytvoří. Pokud soubor existuje, bude jeho původní obsah přepsán novým obsahem.

Jednoduchý příklad otevření souboru.

Jednoduchý příklad
Příklad:

FILE *soubor;

if(soubor = fopen("mujsoubor", "r")) == NULL)
{
 printf("Chyba pri otevirani souboru");
 exit(1);
}

Výše popsaný, v tuto chvíli otevřený soubor, zavřeme jednoduše takto:

Jednoduchý příklad
Příklad:

fclose(soubor);

10.4. Proud a vstup a výstup znaků

Časová náročnost
Časová náročnost: 10 minut

Důležité
  • int fgetc(FILE *stream);

    V případě úspěšného načtení znaku je jeho kód hodnotou návratovou. V případě chyby nebo dosažení konce proudu vrací hodnotu EOF.

  • int ungetc(int c, FILE *stream);

    Je-li c různé od EOF, uloží jej do datového proudu 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í kód vráceného znaku.

  • int fputc(int c, FILE *stream);

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

Pozn:
Pozn:
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.

Na následujícím příkladu si vyzkoušíme základní funkce souborového systému. Nejprve otevřeme soubor MUJ pro zápis. Poté do něj zapíšeme: "Toto je cvicny soubor" a soubor zavřeme. Následně soubor otevřeme v režimu pouze pro čtení a vypíšeme jeho obsah na obrazovku a soubor opět zavřeme.

src/stream_1.c
Příklad 10.1.
/******************************
*	stream_1.c
*	19.03.2002
******************************/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
 char str[80] = "Toto je cvicny soubor";
 FILE *soubor;
 char ch, *p;

 /* otevreni souboru pro zapis */
 if((soubor = fopen("muj", "w")) == NULL)
 {
  printf("Soubor nelze otevrit\n");
  exit(1);
 }

 /* zapis do souboru a jeho ulozeni */
 p = str;
 while(*p)
  if(fputc(*p++, soubor) == EOF)
  {
	printf("Pri zapisu do souboru doslo k chybe\n");
	exit(1);
  }

 fclose(soubor);

 /* otevreni souboru pro cteni */
 if((soubor = fopen("muj", "r")) == NULL)
 {
  printf("Soubor nelze otevrit\n");
  exit(1);
 }


 /* cteni souboru */
 while((ch = fgetc(soubor)) != EOF)
  putchar(ch);

 fclose(soubor);

 return 0;
}

10.5. Proud a vstup a výstup řetězců

Časová náročnost
Časová náročnost: 10 minut

Z proudu nemusíme číst pouze jednotlivé znaky. Můžeme načítat i celé řádky. Jednotlivé řádky jsou ukončeny přechodem na nový řádek. Pro čtení musíme mít k dispozici dostatečně velkou vyrovnávací paměť. Nejčastěji ji získáme pomocí znakového pole. Pro vyšší bezpečnost musíme při použití funkce pro čtení uvést velikost této 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í.

Důležité
  • char *fgets(char *s, int n, FILE *stream);

    Načte řetězec (řádek až po jeho konec včetně znaku konce řádku) 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 řetězec s do datového proudu stream. V případě úspěchu vrátí počet zapsaných znaků (délku řetězce), jinak EOF.

A opět příklad. Naším úkolem bude číst řádky zadávané uživatelem a ukládat je do souboru jehož jméno bylo zadáno jako argument příkazového řádku. Když uživatel zadá prázdný řádek je vstup uzavřen a soubor uložen. Poté je soubor znovu otevřen a zadané řádky ze souboru jsou přečteny a vypsány na obrazovku.

src/stream_2.c
Příklad 10.2.
/******************************
*	stream_2.c
*	19.03.2002
******************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
 char str[80];
 FILE *soubor;

 /* kontrola argumentu prikazoveho radku */
 if(argc != 2)
 {
  printf("Chyba je nutne zadat jmeno souboru");
  exit(1);
 }

 /* otevreni souboru pro zapis */
 if((soubor = fopen(argv[1], "w")) == NULL)
 {
  printf("Soubor nelze otevrit\n");
  exit(1);
 }

 printf("Pro ukonceni zadejte prazdny radek\n");

 do
 {
  printf(": ");
  gets(str);
  strcat(str, "\n"); /* pridani noveho radku */
  if(*str != '\n')
	fputs(str, soubor);
 }
 while(*str != '\n');

 fclose(soubor);

 /* otevreni souboru pro cteni */
 if((soubor = fopen(argv[1], "r")) == NULL)
 {
  printf("Soubor nelze otevrit\n");
  exit(1);
 }

 /* cteni souboru */
 do
 {
  fgets(str, 79, soubor);
  if(!feof(soubor))
	printf(str);
 }
 while(!feof(soubor));


 fclose(soubor);

 return 0;
}

10.6. Proud a formátovaný vstup a výstup

Časová náročnost
Časová náročnost: 8 minut

Nová funkce fprintf(), známá printf() a podobně nová funkce fscanf() a známá scanf(). Je vidět, že nové funkce přidávají k odpovídajícím známým protějškům jen úvodní argument stream.

Pro srovnání:

Důležité
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, ...]);

Pomocí řetězcového a formátovaného výstupu vytvořme několikařádkový textový soubor. Ukážeme si při tom, jak do datového proudu zapíšeme řetězce a naformátované číselné hodnoty.

src/stream_3.c
Příklad 10.3.
/******************************
*	stream_3.c
*	19.03.2002
******************************/

#include <stdio.h>
#include <string.h>

int main(void)
{
 FILE *soubor;
 int i;
 char *s, *jmeno = "soubor1.txt";
 if ((soubor = fopen(jmeno, "wt")) == NULL)
   return 1; 
 s = "Toto je textovy soubor vytvoreny v jazyce C.\n";
 fputs(s, soubor);
 for (i = 0; i<10; i++)
   {
    fprintf(soubor, "%5d", i);
   } 
 fputs("\n", soubor);
 s = "Jeste pridat posledni radek na konec.\n";
 fputs(s, soubor);
 if (fclose(soubor) == EOF)
   return 1; 
 return 0;
}

Výstup:

Toto je textovy soubor vytvoreny v jazyce C.
    0    1    2    3    4    5    6    7    8    9
Jeste pridat posledni radek na konec.

10.7. Další užitečné funkce

Časová náročnost
Časová náročnost: 4 minuty

Důležité
  • int feof(FILE *stream);

    Funkce, umožňující zjistit dosažení konce proudu. Její návratová hodnota je - true (tj. jednička, nebo jen hodnota různá od nuly), nacházíme-li se na konci proudu, nebo - false (nula) - jinak.

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

    Přenese aktuální pozici 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

    .

    Tabulka 10.2.

    IdentifikátorVýznam
    SEEK_SETposun vůči aktuální pozici
    SEEK_CURposun vzhledem počátku
    SEEK_ENDposun vzhledem ke konci

10.8. Binární soubory

Časová náročnost
Časová náročnost: 12 minut

Při práci s binárním proudem je nezbytný blokový přenos dat. Do binárního proudu totiž zapisujeme hodnoty nikoliv pěkně naformátované v textové podobě, ale v binárním tvaru. Tedy, tak jak jsou uloženy v paměti počítače. Výhodou je, že dopředu víme, kolik bajtů je pro který datový typ potřeba.

Skutečnost, že položky binárního souboru jsou stejně veliké, nám umožňuje vypočíst jejich polohu a přečíst, nebo zapsat třeba jedinou hodnotu na určenou pozici. Tomu říkáme náhodný přístup.

Funkce pro práci s binárními soubory:

Důležité
  • fwrite(const void *ptr, size_t size, size_t n, FILE*stream);

    Funkce, která provádí zápis položek do proudu. Má argumenty obdobného významu, jako předchozí funkce pro práci s datovými proudy.Typ size_t, je zaveden pro určení velikosti paměťových objektů a případně počtu.

  • 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 (adresou) 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. I pak jsou načtené položky platné.

Ukažme si nyní program, který naplní pole o deseti prvcích čísly s pohyblivou řádovou čárkou, zapíše je do souboru a znovu přečte. Program zapisuje každý prvek pole zvlášť. Jelikož se binární data zapisují ve svém interním formátu, musí být soubor otevřen pro binární I/O operace.

src/strm_bin.c
Příklad 10.4.
/******************************
*	strm_bin.c
*	19.03.2002
******************************/

#include <stdio.h>
#include <stdlib.h>

double d[10] = {10.23, 19.87, 100.2, 0.258, 
11.15, 95.23, 21.21, 458.03, 73.321, 3.14};

int main(void)
{
 int i;
 FILE *soubor;
 
 if((soubor = fopen("soubor2", "wb")) == NULL)
 {
  printf("Soubor nelze otevrit.\n");
  exit(1);
 }
 
 for(i=0; i<10; i++)
    if(fwrite(&d[i], sizeof(double), 1, soubor) != 1)
	{
	 printf("Chyba pri zapisu.\n");
	 exit(1);
	}
 fclose(soubor);
 
 if((soubor = fopen("soubor2", "rb")) == NULL)
 {
  printf("Soubor nelze otevrit.\n");
  exit(1);
 }	  

 /* vymazani pole */
 for(i=0; i<10; i++) d[i] = 0.0;
 
 for(i=0; i<10; i++)
    if(fread(&d[i], sizeof(double), 1, fp) != 1)
	{
	 printf("Chyba pri zapisu.\n");
	 exit(1);
	}
 fclose(soubor);
 
 /* vypis pole */
 for(i=0; i<10; i++) 
    printf("%f ", d[i]);
	
 return 0;		 
}

Následující příklad používá fseek() pro výpis hodnoty libovolného byte v souboru zadaném na příkazovém řádku.

src/fseek.c
Příklad 10.5.
/******************************
*	fseek.c
*	19.03.2002
******************************/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
 long pozice;
 FILE *soubor;

 /* kontrola argumentu prikazoveho radku */
 if(argc != 2)
 {
  printf("Chyba je nutne zadat jmeno souboru");
  exit(1);
 }

 /* otevreni souboru */
 if((soubor = fopen(argv[1], "rb")) == NULL)
 {
  printf("Soubor nelze otevrit\n");
  exit(1);
 }

 /* vyhledavani */
 printf("Zadejte cislo vyhledavaneho byte: \n");
 scanf("%ld",&pozice);
 if(fseek(soubor, pozice, SEEK_SET))
 {
  printf("Chyba vyhledavani\n");
  exit(1);
 }

 printf("Hodnota na pozici %ld je %d", pozice, getc(soubor));

 fclose(soubor);

 return 0;
}

10.9. Opakování

Cvičení
Cvičení

Úkol k textu

Zadání 1)

Napište program, který vypíše obsah textového souboru, zadaného jako argument příkazové řádky, na obrazovku.
Řešení

Úkol k textu

Zadání 2)

Napište program, který kopíruje textový soubor. Jméno zdrojového a cílového souboru se zadává na příkazovém řádku. Pro kopírování souborů použijte funkce fgets() a fputs(). Proveďte úplnou kontrolu chyb.
Řešení

Úkol k textu

Zadání 3)

Napište program, který umožňuje uživateli zadat libovolný počet hodnot double (max 32 767) a zapsat je do souboru tak, jak jsou zadávány. Tento soubor pojmenujte VALUES. Počet zadaných hodnot zapište do souboru COUNT.
Řešení

Úkol k textu

Zadání 4)

Napište program, který používá soubory ze zadání 3). Program nejprve přečte ze souboru COUNT počet prvků v souboru VALUES. Poté čte a vypisuje hodnoty ze souboru VALUES.
Řešení

Úkol k textu

Zadání 5)

Napište program, který vyhledává v souboru zadaném jako argument příkazového řádku určitou celočíselnou hodnotu zadanou také jako argument příkazového řádku. Je-li hodnota nalezena, nechejte program vypsat její bytovou pozici od začátku souboru.
Řešení

Úkol k textu

Zadání 6)

Napište program, který spočítá počet bytů v souboru (textovém nebo binárním) a zobrazí výsledek. Jméno souboru zadá uživatel jako argument příkazové řádky.
Řešení
test10.swf
Test
Kliknutím na ikonu spustíte test.