25.Prednaska

Z Pascal
Prejsť na: navigácia, hľadanie
25. Prednáška

úlohy | cvičenie


Binárne súbory

V Pascale môžeme so súbormi pracovať 3 rôznymi spôsobmi:

  • starý štýl (staré verzie Pascalu) - sem patrí aj TextFile a tiež file of a file
  • systémová úroveň práce so súbormi (najnižšia úroveň) - Pascal ho využíva pre svoj súborový typ
  • moderný objektový spôsob - trieda TStream, tzv. údajový prúd

Binárny súbor (na rozdiel od textového) údaje uchováva v binárnom tvare, t.j. v tom tvare, ako sú uchované v pamäti počítača počas behu programu (celé čísla v 4 bajtoch a reálne čísla v 8 bajtoch). Napr. čísla v textových súboroch sme uchovávali v textovom tvare, t.j. ako postupnosť znakov desiatkového zápisu, a preto čísla v súbore zaberali rôzny počet znakov.

Binárne súbory sú v Pascale dvoch typov:

  • typové - celý súbor sa skladá z postupnosti hodnôt rovnakého typu, a teda rovnakej dĺžky
  • netypové - súbor je postupnosť hodnôt rôznej štruktúry, a teda asi aj dĺžky
    • aj údajové prúdy sú netypové súbory

Textové súbory fungujú na tzv. sekvenčnom prístupe:

  • otvorenie súboru nastaví vnútorný ukazovateľ na začiatok súboru
  • každé čítanie, resp. zápis automaticky posunú ukazovateľ za prečítané, resp. zapísané údaje - t.j. sekvenčne buď čítame alebo zapisujeme
  • ak by sme napríklad chceli prečítať posledný riadok a pritom sme nastavení na začiatku súboru, musíme najprv prečítať všetky predchádzajúce riadky, až kým sa nedostaneme na posledný

Pre binárne súbory funguje tzv. priamy prístup:

  • môžeme nielen zistiť pozíciu ukazovateľa v súbore, ale ju aj zmeniť
  • každá operácia čítania, resp. zápisu teda pracuje s pozíciou ukazovateľa: z tejto pozície prečíta, resp. na túto pozíciu zapíše, po každej takejto operácii automaticky túto pozíciu posunie za spracovaný údaj (hoci pozíciu môžeme aj tak znovu zmeniť)
  • pozície v súbore počítame od 0 (začiatok súboru) až po koniec-1 (posledná hodnota)
    • máme k dispozícii hodnotu Size, ktorá vráti počet hodnôt v súbore (veľkosť súboru), t.j. vieme sa rýchlo dostať na poslednú hodnotu, resp. na úplný koniec


Sekvenčný prístup

Keďže už máme dobré skúsenosti s textovými súbormi, pripomeňme si ich najdôležitejšie vlastnosti, aby sme mohli lepšie pochopiť špecifiká binárnych súborov:

  • textové súbory pracujú s postupnosťami znakov, resp. riadkov,
  • textový súbor musíme najprv otvoriť buď pre čítanie pomocou Reset alebo na zápis pomocou Rewrite - podľa toho, ako sme súbor otvorili, ďalej môžeme tento súbor buď len čítať, alebo len doňho zapisovať,
  • čítať z textového súboru môžeme nielen jednotlivé znaky, ale vďaka automatickej konverzii aj celé riadky (string), čísla (Integer a Real), logické hodnoty alebo konštanty vymenovaného typu,
  • tiež aj zapisovať do textového súboru môžeme nielen znaky, ale aj reťazce, čísla, logické hodnoty (True a False) a konštanty vymenovaného typu,
  • pre textové súbory môžeme pomocou ReadLn, resp. WriteLn buď preskočiť zvyšok riadka, alebo do súboru vložiť značku konca riadka,

Binárne súbory budeme v Pascale realizovať pomocou údajových prúdov. Využijeme typ TFileStream, ktorý je objektový, a preto k nemu pristupujeme pomocou inštancie, metód a vlastností (property).

Zapíšme si to do tabuľky:

deklarácia var T: TextFile; var F: TFileStream;
priradenie AssignFile(T, meno); F := TFileStream.Create(meno, režim);
otvorenie – čítanie Reset(T); Reset
otvorenie – zápis Rewrite(T); Rerite
či koniec súboru if Eof(T) then Eof
či koniec riadku if Eoln(T) Eoln
čítanie Read(T, premenná); F.ReadBuffer(premenná, veľkosť);
ReadLn(T, premenná); ReadLn
zápis Write(T, výraz); F.WriteBuffer(premenná, veľkosť);
WriteLn(T, výraz); WriteLn
zatvorenie CloseFile(T); F.Free;
špeciality automatická konverzia automatická konverzia

Môžeme vidieť, že filozofia používania textových a binárnych súborov je zatiaľ trochu podobná: aj binárne súbory sú postupnosti nejakých hodnôt, ktoré sú uložené v premenných. Tieto hodnoty buď zapisujeme do súboru (WriteBuffer) alebo ich zo súboru čítame (ReadBuffer). Otvorenie binárneho súboru pomocou konštruktora Create dostáva ako prvý parameter meno súboru a v druhom parametri je režim otvorenia súboru. Budú nám stačiť tieto režimy:

  • fmCreate - vytvor súbor
  • fmOpenRead - otvor existujúci súbor iba na čítanie
  • fmOpenWrite - otvor existujúci súbor iba na zápis
  • fmOpenReadWrite - otvor existujúci súbor na čítanie aj zápis



Zápis do súboru



V ďalších ukážkach budeme s Lazarusom pracovať len v "konzolovom režime" (vytvoríme Nový projekt typu Vlastný program).

Porovnajme použitie textového a binárneho súboru:

var
  Subor: TextFile;
  I: Integer;
begin
  AssignFile(Subor, 'cisla.txt');
  Rewrite(Subor);
  for I := 1000 to 1010 do
    WriteLn(Subor, I);
  CloseFile(Subor);
  WriteLn('hotovo');
  ReadLn;
end.
var
  Subor: TFileStream;
  I: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmCreate);
 
  for I := 1000 to 1010 do
    Subor.WriteBuffer(I, SizeOf(Integer));
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Prvý program vytvorí textový súbor, ktorý obsahuje 11 znakových riadkov po jednom čísle od 1000 do 1010. Keďže vieme, že konce riadkov zaberajú v súbore po 2 znaky (#13#10), tento súbor zaberá 66 bajtov. V ľubovoľnom textovom editore by sme videli týchto 11 čísel. Pozrime sa ale na tento súbor v nejakom šestnástkovom editore (šestnástkové číslo 31 označuje desiatkové číslo 49, a teda znak '1', $0D = 13, $0A = 10, ...):

textový súbor cisla.txt
31 30 30 30 0D 0A 31 30 30 31 0D 0A 30 30 30 32
0D 0A 31 30 30 33 0D 0A 31 30 30 34 0D 0A 31 30
30 35 0D 0A 31 30 30 36 0D 0A 31 30 30 37 0D 0A
31 30 30 38 0D 0A 31 30 30 39 0D 0A 31 30 31 30
0D 0A
  • čo je vlastne reťazec '1000'#13#10'1001'#13#10'1002'#13#10'1003'#13#10'1004'#13#10'1005'#13#10...
  • na prezeranie obsahu binárneho súboru môžete použiť niektorý z hexa-editorov, napr. PSPad editor


Druhý program vytvoril binárny súbor, ktorý sa skladá z postupnosti 11 celých čísel po 4-bajtov. Súbor teda bude na disku zaberať 44 bajtov (opäť sme ho vypísali v šestnástkovej sústave):

binárny súbor cisla.dat
E8 03 00 00 E9 03 00 00 EA 03 00 00 EB 03 00 00
EC 03 00 00 ED 03 00 00 EE 03 00 00 EF 03 00 00
F0 03 00 00 F1 03 00 00 F2 03 00 00
  • všimnite si, že jedno celé číslo je tvorené štvoricou bajtov: tieto sú v pamäti uložené od najnižšieho bajtu, teda prvé štvorbajtové číslo je $000003E8, ďalšie $000003E9, $000003EA, ..., $000003F2 Keby sme ich previedli do desiatkovej sústavy (napr. kalkulačkou vo Windows), dostali by sme čísla 1000, 1001, 1002, ..., 1010.

Príkaz Rewrite na začiatku programu vytvoril prázdny súbor - ak už pred tým súbor s takýmto menom existoval, tak jeho obsah sa týmto príkazom (nenávratne) zrušil. Analogicky funguje otvorenie údajového prúdu pomocou konštruktora Create. Tu sme ako druhý parameter pre režim uviedli fmCreate, ktorý urobí presne to isté ako Rewrite pre textový súbor.



Čítanie súboru



V tejto porovnávacej ukážke prečítame obsahy oboch súborov:

var
  Subor: TextFile;
  I, X: Integer;
begin
  AssignFile(Subor, 'cisla.txt');
  Reset(Subor);
  for I := 1 to 11 do
  begin
    Read(Subor, X);
    WriteLn(X);
  end;
  CloseFile(Subor);
  WriteLn('hotovo');
  ReadLn;
end.
var
  Subor: TFileStream;
  I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
 
  for I := 1 to 11 do
  begin
    Subor.ReadBuffer(X, SizeOf(Integer));
    WriteLn(X);
  end;
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Po spustení oboch programov by ste mohli vidieť úplne rovnaké výsledky: vypísalo sa 11 prečítaných čísel od 1000 do 1010.

Druhý parameter metód ReadBuffer a WriteBuffer je počet čítaných, resp. zapisovaných bajtov. Často to bude práve SizeOf(typ), keď pracujeme s premennou tohto typu, ale v týchto jednoduchých ukážkach môžeme zapísať aj

Subor.WriteBuffer(X, 4);
Subor.ReadBuffer(X, 4);

keďže vieme, že X je celočíselná premenná, ktorá zaberá 4 bajty.

Ešte si uvedomte, že ak chcete do súboru zapísať konštantu alebo hodnotu, ktorú vypočítame, musíme použiť nejakú pomocnú premennú, napr. nebude fungovať

Subor.WriteBuffer(123456789, 4);
for I := 1 to 11 do
  Subor.WriteBuffer(I * 1000, 4);

a treba to zapísať takto:

X := 123456789;
Subor.WriteBuffer(X, 4);
for I := 1 to 11 do
begin
  X := I * 1000;
  Subor.WriteBuffer(X, 4);
end;


V ďalších ukážkach budeme pracovať už len s binárnymi súbormi.



Pozícia v súbore



Z práce s textovými súbormi vieme, že súbor si udržuje špeciálny ukazovateľ, ktorý mu nastavuje miesto v súbore, odkiaľ sa bude čítať, resp. kam sa bude zapisovať. Tento ukazovateľ sa po každom príkaze Read a Write automaticky posúva za spracovaný text (čo je tzv. sekvenčný princíp). Pri textových súboroch k takémuto ukazovateľu nemáme prístup, môžeme ho len nastaviť na začiatok (pomocou Reset alebo Rewrite) alebo môžeme zisťovať, či práve neukazuje na koniec riadka alebo celého súboru (pomocou Eoln alebo Eof).

Pri binárnych súboroch trieda TFileStream poskytuje vlastnosť (property) Position - je to celočíselná hodnota ukazovateľa, t.j. momentálna adresa spracovávaného bajtu v súbore. Na začiatku súboru je to 0 na konci (teda posledný bajt súboru), je to dĺžka súboru - 1.

Do programu, ktorý čítal binárny súbor, pridáme výpis tohto ukazovateľa:

var
  Subor: TFileStream;
  I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
 
  for I := 1 to 11 do
  begin
    Write(Subor.Position:4, ': ');
    Subor.ReadBuffer(X, SizeOf(Integer));
    WriteLn(X);
  end;
  WriteLn('teraz je pozicia = ', Subor.Position);
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

a po spustení

   0: 1000
   4: 1001
   8: 1002
  12: 1003
  16: 1004
  20: 1005
  24: 1006
  28: 1007
  32: 1008
  36: 1009
  40: 1010
teraz je pozicia = 44
hotovo

Pri čítaní prvého čísla mal ukazovateľ hodnotu 0. Po prečítaní tohto čísla sa automaticky posunul o 4 prečítané bajty, a preto má druhé číslo pozíciu 4. Po prečítaní posledného 11. čísla je pozícia v súbore 44, teda je presne rovnaká ako veľkosť súboru. Hovoríme, že pozícia je za koncom súboru (keďže koniec súboru, teda posledný bajt, je 43.)



Práca s poľom čísel



Predchádzajúci príklad s 11 celými číslami prepíšme pomocou jednorozmerného 11-prvkového poľa:

var
  Subor: TFileStream;
  Pole: array [1..11] of Integer;
  I: Integer;
begin
  for I := 1 to 11 do
    Pole[I] := I + 999;
  Subor := TFileStream.Create('cisla.dat', fmCreate);
  for I := 1 to 11 do
    Subor.WriteBuffer(Pole[I], SizeOf(Integer));
  Subor.Free;
 
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  for I := 1 to 11 do
    Subor.ReadBuffer(Pole[I], SizeOf(Integer));
  Subor.Free;
  for I := 1 to 11 do
    WriteLn(Pole[I]);
 
  WriteLn('hotovo');
  ReadLn;
end.

Najprv sme pole iniacializovali hodnotami od 1000 do 1010. Potom sme jeho prvky po jednom zapísali do súboru a súbor sme zatvorili. Ďalej ten istý program tento súbor znovu otvoril, ale teraz už na čítanie (fmOpenRead). Teraz sa zo súboru prečítajú prvky poľa a vypíšu sa do konzolového okna.

Všimnite si, že najprv sme binárny súbor vytvorili pomocou fmCreate a vďaka tomu sme mohli doň zapisovať. Potom sme ho zatvorili a hneď nato opäť otvorili na čítanie fmOpenRead. Lenže, otvorenie súboru s jeho vytvorením (fmCreate) ho vyprázdni a zároveň nastaví režim na čítanie aj zápis, t.j. ako keby bolo zadané fmOpenReadWrite. Jedine, čo musíme urobiť, keď skončíme zápis do súboru a chystáme sa z neho čítať od začiatku, musíme nastaviť pozíciu na úplný začiatok, teda na 0. Urobíme to bežným priradením:

var
  Subor: TFileStream;
  Pole: array [1..11] of Integer;
  I: Integer;
begin
  for I := 1 to 11 do
    Pole[I] := I + 999;
  Subor := TFileStream.Create('cisla.dat', fmCreate);
  for I := 1 to 11 do
    Subor.WriteBuffer(Pole[I], SizeOf(Integer));
  // Subor.Free;
 
  // Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  Subor.Position := 0;
  for I := 1 to 11 do
    Subor.ReadBuffer(Pole[I], SizeOf(Integer));
  Subor.Free;
  for I := 1 to 11 do
    WriteLn(Pole[I]);
 
  WriteLn('hotovo');
  ReadLn;
end.

Pripomeňme si, že pri textových súboroch sme toto isté urobili pomocou príkazu Reset.

Vysvetlime si, ako fungujú metódy WriteBuffer a ReadBuffer:

  • prvý parameter určuje premennú, ktorá sa bude zapisovať, resp. do ktorej sa bude čítať
  • druhý parameter určuje počet bajtov
  • v skutočnosti je pre tieto dva príkazy dôležité nie premenná, ale pamäťové miesto od ktorého sa bude pracovať s pamäťou určenej veľkosti
  • tieto dva príkazy ale nekontrolujú (ani nemôžu) skutočnú veľkosť zapisovanej (čítanej premennej) a je len na programátorovi, aby to správne ustrážil

Takže z celočíselnej premennej môžeme zapísať (alebo do nej prečítať) nie všetky 4 bajty, ale napr. hoci len 1 - vtedy pracujeme len s najnižším bajtom a zvyšné tri sa nepoužijú.

Podobne funguje aj vtedy, keď celočíselnú premennú zapíšeme na viac ako 4 bajty. Vtedy sa pri zápise zapíše nielen jej hodnota, ale aj bajty, ktoré sú v pamäti uložené za touto premennou. Pre jednoduchú premennú nemáme zaručené, že tesne za ňou sa nachádza niečo užitočné, ale jednorozmerné pole je vlastne postupnosť indexovaných premenných, ktoré v pamäti ležia tesne za sebou. Takže, keď do súboru zapíšeme napr. 3. prvok poľa na veľkosť 8 bajtov (2*SizeOf(Integer)), tak sa do súboru zapíšu naraz 3. aj 4. prvok. Takto môžeme naraz zapísať celé pole, ale aj celé pole naraz zo súboru prečítať. Predchádzajúci príklad prepíšeme takto:

var
  Subor: TFileStream;
  Pole: array [1..11] of Integer;
  I: Integer;
begin
  for I := 1 to 11 do
    Pole[I] := I + 999;
  Subor := TFileStream.Create('cisla.dat', fmCreate);
  Subor.WriteBuffer(Pole[1], Length(Pole) * SizeOf(Integer));
 
  Subor.Position := 0;
  Subor.ReadBuffer(Pole[1], 44);
  Subor.Free;
  for I := 1 to 11 do
    WriteLn(Pole[I]);
 
  WriteLn('hotovo');
  ReadLn;
end.

Do súboru sme zapísali prvý prvok poľa ale veľkosti 44 bajtov, t.j. celé pole. Podobne sme čítali len do prvého prvku ale až 44 bajtov, teda sme naraz prečítali do všetkých 11 prvkov. Tieto dva príkazy WriteBuffer a ReadBuffer by sme mohli zapísať aj takto:

  Subor.WriteBuffer(Pole, SizeOf(Pole));
  Subor.Position := 0;
  Subor.ReadBuffer(Pole, SizeOf(Pole));

teda ako parameter posielame celé pole a nie jeden prvok.

Pozor si ale treba dať na dynamické polia. Pozrite tento príklad, ktorý je ale úplne zlý:

var
  Subor: TFileStream;
  Pole: array of Integer;
  I: Integer;
begin
 SetLength(Pole, 1000);
  for I := 0 to High(Pole) do
    Pole[I] := I;
  Subor := TFileStream.Create('cisla1.dat', fmCreate);
  Subor.WriteBuffer(Pole, SizeOf(Pole));
  WriteLn('hotovo');
  ReadLn;
end.

My predsa vieme, že dynamické pole je reprezentované ako smerník do dynamickej pamäte, kde sa nachádzajú samotné prvky poľa. Preto pri zápise do súboru sa nebude zapisovať samotné pole, ale smerník (nejaká adresa) a veľkosť tejto adresy (teda SizeOf(Pole)) je vždy 4. Takto vytvorený súbor bude zaberať iba 4 bajty a bude obsahovať úplne zbytočnú informáciu.

Prvky poľa môžeme zapisovať nielen od prvého, ale aj od ľubovoľného ďalšieho v poli. Zistite, ako bude vyzerať binárny súbor teraz a ako bude dlhý (koľko celých čísel do neho zapíšeme):

var
  Subor: TFileStream;
  Pole: array [1..10] of Integer;
  I: Integer;
begin
  for I := 1 to 10 do
    Pole[I] := I;
  Subor := TFileStream.Create('cisla2.dat', fmCreate);
  for I := 1 to 10 do
    Subor.WriteBuffer(Pole[I], (11 - I) * SizeOf(Integer));
  Subor.Free;
 
  WriteLn('hotovo');
  ReadLn;
end.



Veľkosť súboru



V predchádzajúcom príklade sme do súboru najprv zapísali 10 celých čísel 1..10, potom 9 čísel 2..10, potom 8 čísel 3..10, atď. až na koniec jedno číslo 10. Vieme spočítať, že sme zapísali 55 štvorbajtových čísel, teda súbor cisla2.dat by mal mať 220 bajtov. Túto veľkosť nám pomôže zistiť vlastnosť (property) Size. Podobne ako Position aj Size funguje v bajtoch (a nie v počte zapísaných celých čísel). Vyskúšajme to pre predchádzajúci súbor:

var
  Subor: TFileStream;
begin
  Subor := TFileStream.Create('cisla2.dat', fmOpenRead);
  WriteLn('velkost suboru je ', Subor.Size, ' bajtov');
  Subor.Free;
 
  WriteLn('hotovo');
  ReadLn;
end.

Program naozaj vypíše veľkosť 220 bajtov.

Túto hodnotu môžeme vhodne využiť, keď nevieme počet zapísaných údajov do súboru a potrebujeme ich prečítať všetky. Pri textových súboroch sme mali k dispozícii logickú funkciu Eof, ktorá umožnila čítať dovtedy, kým ešte nebol koniec súboru. Pri údajových prúdoch to musíme robiť pomocou porovnávania Position a Size. Napr. prečítanie všetkých čísel súboru môže vyzerať takto:

var
  Subor: TFileStream;
  X: Integer;
begin
  Subor := TFileStream.Create('cisla2.dat', fmOpenRead);
  while Subor.Position < Subor.Size do
  begin
    Subor.ReadBuffer(X, 4);
    Write(X, ' ');
  end;
  WriteLn;
  Subor.Free;
 
  WriteLn('hotovo');
  ReadLn;
end.

Program teraz vypíše všetkých 55 celých čísel zo súboru.

Property Size môžeme využiť nielen na zisťovanie aktuálnej veľkosti súboru, ale tiež na zmenu tejto veľkosti. Napr. zápis

  Subor.Size := 0;

označuje okamžité vyčistenie celého obsahu súboru.

Takýmto priradením môžeme zo 44 bajtového súboru urobiť napr. 20 bajtový (ponechať len prvých 5 celých čísel):

  Subor.Size := 20;

Alebo skrátiť súbor o posledné celé číslo, t.j. skrátiť súbor o 4 bajty:

  if Subor.Size > 0 then
    Subor.Size := Subor.Size - 4;


Priamy prístup

Predpokladajme, že už máme vytvorený binárny súbor cisla.dat, ktorý obsahuje 11 celých čísel od 1000 do 1010. Vyššie je zobrazený výpis všetkých 44 bajtov v nejakom šestnástkovom editore. Pri čítaní týchto čísel sme pre kontrolu vypisovali aj momentálnu hodnotu Position a videli sme, že každé celé číslo je v tomto súbore na pozícii, ktorá je násobkom 4. Skúsme property Position nie vypisovať, ale meniť.

var
  Subor: TFileStream;
  I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  Subor.Position := 20;
  Subor.ReadBuffer(X, SizeOf(Integer));
  WriteLn(X);
  Subor.Position := 12;
  Subor.ReadBuffer(X, SizeOf(Integer));
  WriteLn(X);
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Tento program vypíše najprv číslo 1005 (na pozícii 20) a potom číslo 1003 (na pozícii 12). Tomuto hovoríme priamy prístup, lebo k údajom v súbore nemusíme pristupovať sekvenčne (postupným čítaním od začiatku až po požadovanú pozíciu), ale priamo zadaním pozície. Napriek tomu, že tieto dve čísla sme čítali priamo, metóda ReadBuffer automaticky posunie pozíciu na nasledovnú hodnotu v súbore - teda správa sa sekvenčne.

Pri určovaní pozície v súbore ale treba byť opatrný, lebo Pascal nijako nekontroluje, či sme zadali správnu adresu začiatku celého čísla. Keď napr. zadáme pozíciu, ktorá nie je násobkom 4, program aj tak prečíta 4 bajty a poskladá sa ako Integer, napr.

var
  Subor: TFileStream;
  I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  Subor.Position := 22;
  Subor.ReadBuffer(X, SizeOf(Integer));
  WriteLn(X, ' $', IntToHex(X, 8));
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Program vypíše (funkcia IntToHex je z unitu SysUtils, nezabudnite ho pridať do uses):

65929216 $03EE0000

Lebo naozaj na pozícii 22 je hodnota 03, na 23 je EE, ...

Špeciálnym prípadom nastavenia pozície pri čítaní je to, keď ešte nie sme na konci súboru (zatiaľ platí Subor.Position<Subor.Size), ale nie je tam už dosť bajtov na prečítanie požadovanej hodnoty. Vieme, že náš testovací súbor cisla.dat má 44 bajtov a posledné celé číslo začína na pozícii 40. Nastavme pozíciu napr. na 41:

var
  Subor: TFileStream;
  I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  Subor.Position := 41;
  Subor.ReadBuffer(X, SizeOf(Integer));
  WriteLn(X, ' $', IntToHex(X, 8));
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Program spadne, lebo metóda ReadBuffer chcela čítať 4 bajty a v súbore boli už len 3.

Namiesto ReadBuffer môžeme použiť metódu funkciu Read. Táto funguje veľmi podobne, ale nespadne pri nedostatku bajtov v súbore. Funkcia vráti počet prečítaných bajtov, a teda môžeme vidieť, že program nepadá, aj keď prečíta zlý vstup:

var
  Subor: TFileStream;
  Pocet, I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  Subor.Position := 37;
  Pocet := Subor.Read(X, SizeOf(Integer));
  WriteLn('precital ', Pocet, ' bajty X = ', X, ' $', IntToHex(X, 8));
  Pocet := Subor.Read(X, SizeOf(Integer));
  WriteLn('precital ', Pocet, ' bajty X = ', X, ' $', IntToHex(X, 8));
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

V tejto ukážke čítame dve 4-bajtové celé čísla: prvé od pozície 37 a druhé od pozície 41. Z výpisu programu vidíme, že prvé číslo sa prečítalo na 4 bajty, druhé len na 3:

precital 4 bajty X = -234881021 $F2000003
precital 3 bajty X = -234881021 $F2000003
hotovo

Metódu Read používame len vtedy, keď nevieme presne počet bajtov, ktoré bude treba prečítať, inak používame iba ReadBuffer. Read sa dá používať aj ako procedúra, t.j. zanedbáme výsledok funkcie, ale to znamená, že nekontrolujeme správnosť čítania, a teda zanedbáme správnosť prečítaných údajov. Toto je ale programátorsky veľmi nezodpovedné. Preto, keď používame Read namiesto ReadBuffer a nekontrolujeme, či prebehlo korektné čítanie, pracujeme s chybnými “prečítanými” hodnotami bez informácie o chybe.

Priamy prístup môžeme využiť napr. aj na čítanie všetkých údajov v inom poradí ako sekvenčne. Napr. môžeme všetky čísla zo súboru prečítať v opačnom poradí:

var
  Subor: TFileStream;
  Pocet, I, X: Integer;
begin
  Subor := TFileStream.Create('cisla.dat', fmOpenRead);
  for I := Subor.Size div 4 - 1 downto 0 do
  begin
    Subor.Position := 4 * I;
    Subor.ReadBuffer(X, 4);
    WriteLn(X);
  end;
  Subor.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Zmenu pozície môžeme využiť nielen pri čítaní súboru, ale aj pre zápise. V nasledujúcej ukážke predpokladáme, že máme binárny súbor subor.dat, v ktorom sú kladné aj záporné celé čísla. Úlohou je prepísať v tomto súbore všetky záporné hodnoty na ich absolútne hodnoty:

var
  F: TFileStream;
  X: Integer;
begin
  F := TFileStream.Create('subor.dat', fmOpenReadWrite);
  while F.Position < f.Size do
  begin
    F.ReadBuffer(X, SizeOf(Integer));
    if X < 0 then
    begin
      X := -X;
      F.Position := F.Position - SizeOf(Integer);
      F.WriteBuffer(X, SizeOf(Integer));
    end;
  end;
  F.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Keď sa zo súboru prečíta záporné číslo, tak sa pozícia vráti o 4 bajty späť a na túto adresu sa zapíše (teda prepíše pôvodná) absolútna hodnota prečítaného čísla. Po tomto zápise čísla sa pozícia v súbore automaticky posunie na ďalšiu hodnotu, ktorá sa zrejme v ďalšom prechode cyklu prečíta.



Ukladanie reťazcov do súboru



Ak chceme do binárneho súboru uložiť niekoľko znakových reťazcov, mali by sme ich navzájom nejako oddeliť, aby sme ich mohli neskôr znovu prečítať. Veľmi často sa používa taký spôsob zápisu reťazcov, že sa najprv zapíše jeho dĺžka (ako 4-bajtové celé číslo) a za tým samotný reťazec. Takýto zápis má takú výhodu, že okrem takto jednoduchého zápisu do súboru sa reťazec dá aj veľmi jednoducho a rýchlo prečítať. Nasledujúci program zapíše do súboru dva reťazce:

var
  F: TFileStream;
  S: string;
  Dlzka: Integer;
begin
  F := TFileStream.Create('texty.dat', fmCreate);
  S := 'programovanie';
  Dlzka := Length(S);
  F.WriteBuffer(Dlzka, SizeOf(Integer));
  //if S <> 0 then
    F.WriteBuffer(S[1], Dlzka);
 
  S := 'Lazarus';
  Dlzka := Length(S);
  F.WriteBuffer(Dlzka, SizeOf(Integer));
  //if S <> 0 then
    F.WriteBuffer(S[1], Dlzka);
 
  F.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Ak by sme nemali istotu, že reťazec nie je prázdny, mali by sme pred samotným zápisom reťazca, otestovať či nie prázdny. Totiž volanie F.WriteBuffer(S[1], Dlzka); pre prázdny reťazec spadne na chybe (pre prázdny reťazec nemôžeme zapísať S[1]).

Prečítanie reťazcov je potom takto jednoduché:

var
  F: TFileStream;
  S: string;
  Dlzka: Integer;
begin
  F := TFileStream.Create('texty.dat', fmOpenRead);
  while F.Position < F.Size do
  begin
    F.ReadBuffer(Dlzka, SizeOf(Integer));
    SetLength(S, Dlzka);
    //if S <> 0 then
      F.ReadBuffer(S[1], Dlzka);
    WriteLn('retazec = "', S, '"');
  end;
  F.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Trieda TFileStream ponúka ešte varianty príkazov ReadBuffer a WriteBuffer, ktoré zjednodušujú a sprehľadňujú niektoré zápisy. Napr. na prečítanie alebo zápis celého 4-bajtového čísla môžeme použiť metódy (funkcia) ReadDWord a WriteDWord (hoci v skutočnosti sa nepracuje s Integer ale s Cardinal, čo je nezáporné 4-bajtové číslo). Teraz by sme zápis a prečítanie reťazca mohli zapísať takto:

var
  F: TFileStream;
  S: string;
begin
  F := TFileStream.Create('texty.dat', fmCreate);
  S := 'programovanie';
  F.WriteDWord(Length(S));
  F.WriteBuffer(S[1], Length(S));
  ...

a čítanie

var
  F: TFileStream;
  S: string;
begin
  F := TFileStream.Create('texty.dat', fmOpenRead);
  while F.Position < F.Size do
  begin
    SetLength(S, F.ReadDWord);      // ReadDWord je funkcia
    //if S <> 0 then
      F.ReadBuffer(S[1], Length(S));
    ...

Na podobnom princípe fungujú aj ďalšie metódy, napr. ReadByte, ReadWord, WriteByte, WriteWord pre jedno a dvojbajtové nezáporné čísla.

Dokonca pre znakové reťazce existujú priamo metódy na zápis aj čítanie presne v tom istom formáte, ako sme to robili tu. Ich použitie môžeme vidieť na príklade zápisu a potom čítania:

var
  F: TFileStream;
begin
  F := TFileStream.Create('texty.dat', fmCreate);
  F.WriteAnsiString('programovanie');
  F.WriteAnsiString('Lazarus');
  F.Free;
  WriteLn('hotovo');
  ReadLn;
end.

a čítanie

var
  F: TFileStream;
begin
  F := TFileStream.Create('texty.dat', fmOpenRead);
  while F.Position < F.Size do
    WriteLn('retazec = "', F.ReadAnsiString, '"');      // ReadAnsiString je funkcia
  F.Free;
  WriteLn('hotovo');
  ReadLn;
end.



kopírovanie súboru



Ukážme, kopírovanie jedného binárneho súboru do druhého. V prvej verzii budeme kopírovať po jednom bajte:

var
  Subor1, Subor2: TFileStream;
  Bajt: Byte;
begin
  Subor1 := TFileStream.Create('novy.dat', fmCreate);
  Subor2 := TFileStream.Create('subor.dat', fmOpenRead);
  while Subor2.Position < Subor2.Size do
    Subor1.WriteByte(Subor2.ReadByte);
  Subor1.Free;
  Subor2.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Takéto kopírovanie je pre väčší súbor veľmi pomalé, a preto je zvykom použiť pomocný buffer, napr. takto

var
  Subor1, Subor2: TFileStream;
  Buffer: array [1..1000] of Byte;
  Pocet: Integer;
begin
  Subor1 := TFileStream.Create('novy.dat', fmCreate);
  Subor2 := TFileStream.Create('subor.dat', fmOpenRead);
  repeat
    Pocet := Subor2.Read(Buffer, Length(Buffer));
    if Pocet > 0 then
      Subor1.WriteBuffer(Buffer, Pocet);
  until Pocet < Length(Buffer);
  Subor1.Free;
  Subor2.Free;
  WriteLn('hotovo');
  ReadLn;
end.

Od veľkosti pomocného buffra teraz závisí rýchlosť kopírovania.

Metóda CopyFrom slúži práve na kopírovanie súboru. Použiť ju môžeme napr. takto:

var
  Subor1, Subor2: TFileStream;
begin
  Subor1 := TFileStream.Create('novy.dat', fmCreate);
  Subor2 := TFileStream.Create('subor.dat', fmOpenRead);
  Subor1.CopyFrom(Subor2, Subor2.Size);
  Subor1.Free;
  Subor2.Free;
  WriteLn('hotovo');
  ReadLn;
end.


Príklad s kresbou v súbore

Ideme riešiť takúto úlohu: pri ťahaní myšou po grafickej ploche chceme vytváranú kresbu ukladať do binárneho súboru. V súbore budú teda dvojice súradníc X, Y najlepšie ukladané pomocou TPoint (čo je record X, Y: Integer; end). Ak predpokladáme, že kresba v súbore nemusí byť vytvorená jedným ťahom, ale sa vytvárala niekoľkými onMouseDown a onMouseMove, tak aj v súbore to treba nejako zaznačiť (tu končí jedna čiara a začína ďalšia). Zvolíme takýto zápis do súboru:

  • každý súvislý úsek kresby začne dĺžkou úseku (počet bodov kresby a nie počet bajtov)
  • za tým nasleduje príslušný počet bodov (t.j. štruktúry TPoint)
  • za tým môže nasledovať ďalšia súvislá časť kresby, atď.

Pri vytváraní kresby budeme najprv samotné body ukladať do dynamického poľa prvkov TPoint a pri pustení myši (onMouseUp) toto pole uložíme na koniec súboru. Prvá aplikácia vytvára binárny súbor:

var
  Pole: array of TPoint;
 
procedure TForm1.FormCreate(Sender: TObject);
var
  F: TFileStream;
begin
  Image1.Canvas.FillRect(Image1.ClientRect);
  F := TFileStream.Create('body.dat', fmCreate);      // vymaž obsah súboru
  F.Free;
end;
 
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  SetLength(Pole, 1);                                 // vyprázdni pole bodov
  Pole[0] := Point(X, Y);
  Image1.Canvas.MoveTo(X, Y);
end;
 
procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  if Shift = [ssLeft] then
  begin
    SetLength(Pole, Length(Pole) + 1);
    Pole[High(Pole)] := Point(X, Y);
    Image1.Canvas.LineTo(X, Y);
  end;
end;
 
procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  F: TFileStream;
begin
  if Length(Pole) > 1 then
  begin
    F := TFileStream.Create('body.dat', fmOpenWrite);     // zapíš na koniec súboru
    F.Position := F.Size;
    F.WriteDWord(Length(Pole));
    F.WriteBuffer(Pole[0], Length(Pole) * SizeOf(TPoint));
    F.Free;
  end;
  Pole := nil;
end;

Druhá aplikácia prečíta kresbu uloženú v súbore a vykreslí ju pomocou PolyLine:

procedure TForm1.Image1Click(Sender: TObject);
var
  P: array of TPoint;
  F: TFileStream;
begin
  Image1.Canvas.FillRect(Image1.ClientRect);
  F := TFileStream.Create('body.dat', fmOpenRead);
  while F.Position < F.Size do                     // kým nie je koniec súboru
  begin
    SetLength(P, F.ReadDWord);                     // prečítaj dĺžku a vytvor tak veľké pole
    F.ReadBuffer(P[0], Length(P) * SizeOf(TPoint));
    Image1.Canvas.Polyline(P);
  end;
  F.Free;
end;

Cvičenie:

  • pre daný súbor body.dat, ktorý obsahuje nejakú kresbu, zistite z koľkých súvislých úsekov sa skladá a aká je dĺžka najdlhšieho úseku (nepoužívajte pri tom žiadne polia)


Zhrnutie TFileStream

Trieda TFileStream slúži na prácu s binárnymi súbormi. Umožňuje pracovať s binárnymi údajmi (čítať a zapisovať) ľubovoľnej štruktúry, pritom sa dá zisťovať momentálna dĺžka súboru a tiež momentálna pozícia do súboru.

Zjednodušene by sme si jeho deklaráciu mohli predstaviť takto:

type
  TFileStream = class(TStream)
    constructor Create(const AFileName: string; Mode: Word);
    procedure ReadBuffer(var Buffer; Count: Integer); override;
    function Read(var Buffer; Count: Integer): Integer; override;
    function ReadByte: Byte; override;
    function ReadWord: Word; override;
    function ReadDWord: Cardinal; override;
    function ReadAnsiString: string; override;
    procedure WriteBuffer(const Buffer; Count: Integer); override;
    function Write(const Buffer; Count: Integer): Integer; override;
    procedure WriteByte(AByte: Byte); override;
    procedure WriteWord(AWord: Word); override;
    procedure WriteDWord(ADWord: Cardinal); override;
    procedure WriteAnsiString (const S: string); override;
    function CopyFrom(Source: TStream; Count: Int64): Int64; override;
 
    property Position: Int64 read GetPosition write SetPosition;
    property Size: Int64 read GetSize write SetSize64;
    property FileName: String Read FFilename;
  end;



Deklarácia



Pre prácu s údajovým prúdom zadeklarujeme objektovú premennú, napr.

var
  Subor: TFileStream;



Vytvorenie inštancie - otvorenie súboru



Objektovú premennú musíme skonštruovať napr. takto

  Subor := TFileStream.Create(Meno, Rezim);

kde Meno je meno súboru (rovnako ako pri AssignFile pre textové súbory) a Rezim je jedna z týchto možností:

  • fmCreate - vytvor súbor - ak taký už existoval, najprv vymaže jeho pôvodný obsah
  • fmOpenRead - otvor súbor iba na čítanie - tento súbor už musí existovať
  • fmOpenWrite - otvor súbor iba na zápis - tento súbor už musí existovať
  • fmOpenReadWrite - otvor súbor na čítanie aj zápis - tento súbor už musí existovať

Najbežnejšie režimy pre nás budú fmCreate a fmOpenReadWrite, ktoré oba umožňujú do otvoreného súboru zapisovať aj z neho čítať.



Čítanie zo súboru



Základnou metódou je ReadBuffer: prvý parameter je pamäťové miesto (najčastejšie premenná, nesmie to byť smerník!) a druhý parameter je počet prečítaných bajtov. Predpokladá sa, že príslušný počet bajtov je naozaj v súbore (inak spadne na chybe, t.j. vyvolá exception) a že na príslušné pamäťové miesto sa prečítané bajty zmestia (inak prepíšeme nejakú "cudziu" pamäť a program môže spadnúť ...). Napr.

  Subor.ReadBuffer(Premenna, SizeOf(Premenna));

Metóda Read funguje podobne ako ReadBuffer, ale keďže je funkciou, vracia počet prečítaných bajtov a v prípade, že v súbore už nebol požadovaný počet, vráti nám menšie číslo, napr.

  if Subor.Read(X, 4) <> 4 then WriteLn('chyba pri citani');

Túto funkciu môžeme volať aj ako procedúru, ale vtedy v prípade chyby program nespadne, ale pokračuje so zle prečítanou hodnotou. Napr.

  Subor.Read(X, 4);

v prípade chyby program normálne pokračuje bez varovania a v premennej X je pravdepodobne nejaký nezmysel.

Ďalšie 4 metódy sú funkcie, ktoré prečítajú jednu hodnotu zo súboru a túto vrátia ako výsledok funkcie. V prípade chyby, program spadne. Použitie napr.

  Bajt := Subor.ReadByte;            // číta 1 bajt
  Slovo := Subor.ReadWord;           // číta 2 bajty
  Cele := Subor.ReadDWord;           // číta 4 bajty
  Retazec := Subor.ReadAnsiString;   // číta 4 bajty + dĺžka reťazca

Pritom predpokladáme, že reťazec je v súbore uložený tak, že najprv ide 4-bajtové celé číslo, ktoré vyjadruje dĺžku reťazca a za tým nasleduje samotný reťazec.



Zápis do súboru



Základnou metódou je WriteBuffer: prvý parameter je pamäťové miesto (najčastejšie premenná, nesmie to byť smerník!) a druhý parameter je počet zapisovaných bajtov. Predpokladá sa, že príslušný počet bajtov sa zmestí do súboru (inak spadne na chybe, t.j. vyvolá exception). Napr.

  Subor.WriteBuffer(Premenna, SizeOf(Premenna));

Metóda Write funguje podobne ako WriteBuffer, ale keďže je funkciou, vracia počet zapísaných bajtov a v prípade, že do súboru sa už požadovaný počet nezmestí (napr. plný disk), vráti nám toto menšie číslo zapísaných bajtov, napr.

  if Subor.Write(X, 4) <> 4 then WriteLn('chyba pri zápise');

Túto funkciu môžeme volať aj ako procedúru, ale vtedy v prípade chyby program nespadne ale pokračuje bez upozornenia ďalej. Napr.

  Subor.Write(X, 4);

Ďalšie 4 metódy zapisujú hodnoty do súboru, pričom tieto nemusia byť uložené v premenných. Použitie napr.

  Subor.WriteByte(5 * 5);            // zapíše 1 bajt
  Subor.WriteWord(100 * 100);        // zapíše 2 bajty
  Subor.WriteDWord(123456789);       // zapíše 4 bajty
  Subor.WriteAnsiString('Pascal');   // zapíše 4 + 6 bajtov

Pritom reťazec sa do súboru uloží tak, že najprv sa zapíše 4-bajtové celé číslo, ktoré vyjadruje dĺžku reťazca a za tým nasleduje samotný reťazec.



Veľkosť súboru



Property Size nám oznámi momentálnu veľkosť súboru. Do Size môžeme aj priradiť nejakú hodnotu, ktorá určí novú veľkosť súboru, napr.

  WriteLn('velkost suboru = ', Subor.Size);
  Subor.Size := Subor.Size div 2;



Pozícia v súbore



Property Position nám oznámi momentálnu pozíciu v súbore. Do Pozition môžeme aj priradiť nejakú hodnotu, ktorá určí novú pozíciu v súbore, napr.

  WriteLn('pozicia = ', Subor.Position);
  Subor.Position := Subor.Size - Subor.Position;



Kopírovanie súboru



Slúži na kopírovanie časti iného súboru do nášho. Ak máme dva už otvorené súbory napr. Subor1 a Subor2, tak môžeme zapísať

  Pocet := Subor1.CopyFrom(Subor2, 0);

Kompletný obsah Subor2 sa prekopíruje do Subor1 - sem sa začne zapisovať od momentálnej pozície (Subor1.Position). Výsledkom volania (v premennej Pocet) bude teraz počet kopírovaných bajtov, t.j. dĺžka Subor2, teda Subor2.Size.

Ak zo Subor2 chceme kopírovať len jeho časť, použijeme aj druhý parameter metódy CopyFrom. Napr.

  Subor2.Position := 100;
  Pocet := Subor1.CopyFrom(Subor2, 200);

označuje, že zo Subor2 sa začne kopírovať od pozície 100 a to presne 200 bajtov. Výsledkom volania bude zrejme číslo 200. Ak by v Subor2 od pozície 100 už nebolo aspoň 200 bajtov, program spadne na chybe (rovnako ako by spadlo Subor2.ReadBuffer(Buf, 200)).

Pomocou metódy CopyFrom môžeme jednoducho poskladať náš binárny súbor z častí aj viacerých súborov

Všimnite si, že parametrom nie je TFileStream ale iba TStream, t.j. predok TFileStream. Ak uvážime, že TStream môže mať viac rôznych potomkov (napr. veľmi užitočný TMemoryStream), tak pomocou tejto metódy, môžeme kopírovať nielen binárne súbory, ale aj iné údajové prúdy.


späť | ďalej