Struktury

Zatím jsme se zabývali jen jednoduchými typy proměnných. Nyní se podíváme na to, jak dát proměnným logickou strukturu.

Jelikož programování není suchá teorie, ale spíš praktická činnost, bude důležitá látka vysvětlována na příkladech.

Přehled:

Historie programování

Začněme trochu historií (dnešní lekce tak bude srozumitelnější)
Nejprve se objevilo tzv. lineární programování (příkazy následovaly za sebou a program nešlo řídit.)
Poté přišlo programování skokové (program šlo k rozhodování donutit velice těžce).
Pak se objevily proměnné (sice byly už předtím, ale v poněkud jiné, hůře ovladatelné formě)
Někdo také vymyslel lepší způsob, jak ovládat (řídit) program - řízené programy.
Standardní typy proměnných brzy nestačily, a tak se vymyslelo programování strukturované.
Ovšem ani struktury nestačily a objevily se objekty.
Ty umožnily vizualizaci tvorby programu a tzv. programování událostí.
V poslední době se pak objevila spousta nových metod, jak napsat program (extrémní programování a jiné).

Struktury

Občas potřebujeme nadefinovat několik různých proměnných, které však spolu souvisí. Pro lepší orientaci pak tento souhrn označíme jako jedinou proměnnou a té dáme vlastní název. Ona složená proměnné se pak nazývá struktura. (Správně se mluví o proměnné strukturovaného typu).

Opakování z let minulých

Zopakujme si některé poznatky:
Proměnnou definujeme takto:
var Název : TypProměnné; (takto je to nejjednodušší)
var Název1, Název2 : TypProměnné; (pochopitelně lze udat více názvů, oddělených čárkou)
Pokud jenom definujeme proměnné, stačí vypsat var jen jednou. Nejčastěji se tedy setkáte s něčím takovýmto:

type      TOsoba = Word;  {Třeba výše příjmu - maximum 65 000 snad bude stačit}
var       I,J   : integer;
          RZ,IZ : Real;
 Jmeno,Prijmeni : string[25];
 Znak1, Znak2   : char;
          K     : extended;
          Veta  : string;
          Lubos : TOsoba;

Bývá dobrým zvykem, že definice typů se uvedou před definice proměnných. Zopakujme si ještě, jak takový typ vlastně definovat:
type NázevTypu = PopisTypu;
My jsme se zatím setkali pouze s několika jednoduchými způsoby, jak typ popsat - pomocí jiného, již známého typu, pomocí výčtu a jako interval z již známého typu. Je nám tedy jasný následující zápis:

type CeleCislo = Integer;
     PrirozeneCislo = Word;
     Dny = (pondeli,utery,streda,ctvrtek,patek,sobota,nedele);
     PracovniDen=pondeli..patek;
     Vikend = sobota..nedele;
     MalaPismena = 'a'..'z';

Nejdříve probereme ukazatele, ty stojí na hranici mezi jednoduchými a strukturovanými typy.

Ukazatel

Jak je proměnná uložena v paměti počítače? Hodnota proměnné spí někde v paměti a je vzbuzena, provádí-li se něco s proměnnou.
Občas se nám ale hodí vědět, kde proměnná sídlí, mít její adresu, ukazatel na cestu k jejímu skrovnému domečku. A naopak, máme-li ukazatel, chtěli bychom občas také vědět, jaká hodnota na dané adrese přebývá.

Každá proměnná je jenom shlukem jedniček a nul. Chceme-li z nich vytěžit nějakou informaci, musíme vědět, co vlastně znamenají, proto Pascal zavádí typy proměnných.
(Například : Proměnná typu byte o hodnotě 159 a proměnná typu shortint o hodnotě -97 vytvoří v paměti úplně shodný shluk jedniček a nul... - typy tedy musíme rozlišovat)

Rozlišujeme-li typy u proměnných, měli bychom rozlišovat typy také u ukazatelů. Ukazatel tedy definujeme na určitý datový typ. Tato definice se provádí takto:
type Nazev = ^DatovyTyp;

Příklady: type Ukazatel = ^integer; {A máme ukazatel na integer}
Uk1 = ^Ukazatel; {I ukazatel je proměnná a lze na něj ukazovat}
var A:Ukazatel;
B:^Uk1; {Ukazatel lze definovat i přímo ve var, ale není to vhodné. Ovšem i teď se definuje pomocí ^typ.}

Je zvykem, že názvy ukazatelů začínají písmenem P (z anglického pointer). Můžeme se tedy setkat s tímto:
type Zlodej = string;
PZlodej = ^Zlodej;
var Pachatel : PZlodej; {Kupodivu, zde to P ve slově Pachatel ukazatel neznačí}

Už umíme ukazatel nadefinovat a víme, co to je. Jak s ním zacházet? Pochopitelně je možné přiřazení.
Ukazatel1 := Ukazatel2;
Také můžeme změnit hodnotu proměnné, na kterou ukazatel ukazuje. To se provádí takto :
Pachatel^ := 'zahradnik'; (pomocí znaku ^ tzv. zpřístupníme proměnnou).
Pochopitelně je možné nastavit ukazatel na danou proměnnou:
Ukazatel := @Proměnná;
Existuje také ukazatel, který nikam neukazuje. Říká se mu nil.
Ukazatel :=nil;
(nikam neukazuj)
Pro pochopení si vše projděte na tomto jednoduchém příkladu.

program Demo;
type PInt = ^Integer;
var  Ukaz : PInt;
        I : Integer;
begin
 I:=15;
 Ukaz:=@I; {Ukazatel ted bude ukazovat na I}
 Writeln(Ukaz^);
 Ukaz^ := -10;
 Writeln(Ukaz^);
 Readln;
end.

Pozorně si pročtěte text o ukazatelích a důkladně si tento příklad projděte.
(Pozn: V Pascalu existují také ukazatele bez udaného typu - tzv. pointry, které se definují takto:
var X : pointer; nebo také type PX = pointer; S těmito ukazateli se hůře pracuje, takže o nich až někdy příště.)

Typy struktur

Pascal rozeznává hned několik druhů struktur: množinu, pole, záznam a soubor.

Množina

Množina je určitý soubor prvků stejného typu.
Bázový typ = základní nestrukturovaný typ, jehož hodnot mohou prvky množiny nabývat. Nesmí obsahovat více jak 256 přípustných hodnot a nesmí obsahovat hodnoty záporné.
Typ Množina se definuje takto Název = set of BázovýTyp;
Raději na příkladu:
type Cisla = set of byte;
TDen = (pondeli,utery,streda,ctvrtek,patek,sobota,nedele);
TDny = set of TDen;
var Dny = TDny;
JineDny = TDny;
Nyní se podívejme, jak se s množinou pracuje:
Prázdná množina se značí [ ] (levá hranatá závorka, pravá hranatá závorka - bez mezery!!!). Prázdnou množinu lze přiřadit jakékoliv jiné množině.
Množiny stejného bázového typu lze vzájemně přiřazovat.( Dny := [ ], JineDny :=Dny )
Množinu lze také označit výčtem prvků, které do ní patří (pomocí hranatých závorek ) :
Dny := [pondeli,utery,streda,ctvrtek,patek];
JineDny := [ctvrtek,sobota,nedele];
Máme-li dvě množiny stejného typu, lze s nimi dále provádět tyto operace:

Operace Operátor Výsledek
sjednocení + množina
průnik * množina
rozdíl - množina
srovnání (jsou shodné?) = boolean
srovnání (jsou rozdílné?) <> boolean
je A podmnožinou B? <= boolean
je B podmnožinou A? >= boolean

Příklad:

Dny := [pondeli];
JineDny := [utery];
JineDny := Dny + JineDny; {Poté bude JineDny = [pondeli,utery] }
Dny := Dny + Dny; {Teď máme Dny = [pondeli] }
Dny := Dny - JineDny; {Dny = [], prvky, které se nemají od čeho odečíst se neodečtou}
if Dny = JineDny then writeln('Tyto množiny jsou shodné, což je blbost');
if Dny <> JineDny then writeln('Množiny Dny a JineDny jsou dvě rozdílné množiny);
if Dny <= JineDny then writeln('Prázdná množina Dny je podmnožinou jednoprvkové množiny JineDny');
if JineDny >= JineDny then writeln('Každá množina je svou vlastní podmnožinou');

Také můžeme pomocí in zjistit, zda prvek patří do dané množiny :
if pondeli in JineDny then writeln('Pondělí je jiný den');

Jednotlivý prvek lze přidat či odebrat pomocí + a - (Dny=Dny+[pondeli], nezapomeňte na hranaté závorky!). Mnohem efektivnější je však použít těchto procedur:
Include(S:Množina, I : PrvekMnožiny); Přidá prvek do množiny (stejně jako S:=S+[I] )
Exclude(S:Množina, I : PrvekMnožiny); Odebere prvek z množiny (stejně jako S:=S-[I] )

Rád bych připomněl, že u množin nezáleží na pořadí prvků
Tím bychom množiny skončili. Pročtěte si je ještě jednou a procvičte na příkladech. Pak čtěte dál. Nejčastější použití množin je při ověřování, zda nějaký prvek splňuje určitou podmínku (je velké písmeno,...)

Pole

Množina je typ neuspořádaný. Pole se snaží vnésti do toho trochu pořádku. Definuje se takto:
type Název = array[DolníMez .. HorníMez] of LibovolnýTyp;
DolniMez a HorniMez musí být stejného ordinálního typu.
Pole je jakýsi soubor prvků jednotného typu, kde jsou jednotlivé prvky řazeny za sebou. Hodnoty se mohou opakovat. Zjednodušeně lze říct, že pole obsahuje několik prvků s různými indexy (x[1], x[2],...) Raději na příkladu. Snažte se z něj pochopit i způsob zacházení s polem:

program Pole;
type TPoleS=array[1..8] of Integer;
     TPoleR=array['a'..'h'] of Integer;
     TPoleF=array[1..64] of char;
var Sloupec,S: TPoleS;
    Radka    : TPoleR;
    Figura   : TPoleF;
    I        : integer;
    C        : char;
begin
 Sloupec[1]:= 15;
 Sloupec[2]:= -7;
 Sloupec[3]:= 15;
 Radka['a']:=Sloupec[3];
 Radka['h']:=Sloupec[2];
 Figura[64]:='a';
 Figura[38]:='g';
 Writeln(Figura[64],Radka[3]);
 I:=8;
 C:='a';
 Sloupec[I]:=11;
 Radka[C]:=Sloupec[I-2];
end.

Tento příklad si důkladně prostudujte. Nyní byste již měli vědět, jak se s takovým polem zachází.

Jelikož pole může být definováno z jakéhokoliv typu, existuje i pole polí (a pole polí polí...) V takovýchto případech Pascal nabízí jednodušší způsob zápisu:
array[a..b] of array of [c..d] of Něco se zkracuje jako array[a..b,c..d] of Něco.
Zcela analogicky můžeme psát array[a..b,c..d,e..f,g..h] of string;
S rozměry pole rostou nároky na paměť. Velice rychle se může stát, že nebude stačit. (Turbo Pascal využívá jen 65kB paměti, jiné verze trošku víc)

Pole stejných parametrů jdou sobě přiřazovat. Jinak se musí s poli pracovat prvek po prvku. K tomu se často využívá cyklu for.
Nyní již jste schopni vyřešit následující příklad:
Příklad : Napište program, který seřadí vzestupně 25 uživatelem zadaných hodnot typu integer.
Pokud máte řešení, vyzkoušejte, jestli program funguje a poté ho srovnejte se zde uvedeným řešením. Jestliže jste úkol ani po několika dnech nevyřešili, čtěte dále:

program InsertSort;
const rozsah = 100;
type Tpole = array[1..rozsah] of Integer;
var A:Tpole;
  I,J:integer;
  min:integer;
  kde:integer;

procedure Prohod(var A:integer;var B:integer);
{Procedura k prohozeni dvou promennych}
var C:integer;
begin
 C:=A;
 A:=B;
 B:=C;
end;

procedure Vypis(pole:Tpole);
var I:integer;
begin
 for I:=1 to rozsah do
  write(Pole[I]:8);
writeln;
end;

begin
 Randomize;
 for I:=1 to rozsah do
  A[I]:=Random(Maxint);
 Vypis(A);

 for I:=1 to rozsah do
  begin
   min:=Maxint;
   J:=I;
    for J:=I to rozsah do {Hledame nejmensi prvek a ten dame na prvni misto}
     if min>A[j] then
      begin
       min:=A[j];
       kde:=j;
      end;
    Prohod(A[I],A[kde]);
  end;            {pak dame druhy nejmensi na druhe, pole prohledavame
                    az od druheho mista, nebot na prvnim jiz je nejmensi prvek,
                    a tak pokracujeme dale}
 Vypis(A);
 readln;
end.

Tento postup si zapamatujte, bude se vám později hodit. (Ale existují i lepší.) Program nechávám bez nadbytečných komentářů, abyste si mohli každý řádek samostatně zdůvodnit. (Např. rozsah je řešen jako konstanta, aby mohl být kdykoliv snadno změněn...). Až vše zdůvodníte, bude pro vás hračka napsat program, který vybere ze 100 celých čísel to nejmenší.
(Tip: Vytvořte proměnnou Min = MaxInt a porovnejte ji s každým prvkem pole.)

Pokuste se ještě napsat program, který vytvoří šachovnici (array['a'..'h',1..8] of char) a rozmístí na ní šachové figurky jako při zahájení. Využijte přitom běžné označení šachových figurek (P = pěšec, J=jezdec,...) Barvy rozlišovat nemusíte (ale pokud chcete, můžete pro černé figurky zvolit malá písmena)... Dobrý programátor ví vždy všechno, nebo alespoň ví, kde to najít. Součástí programování je i vyhledávání informací, takže pokud nevíte, jak se značí šachové figurky či kde stojí při zahájení, využijte přátel či odborné literatury...

Tím máme pole za sebou, i když se k nim určitě vrátíme při opakování.

Záznam

Záznam spojuje několik proměnných, které mají logickou souvislost. Tyto proměnné nemusí být stejného typu. Záznam se definuje takto:

type Název = record
              Proměnná1 : TypProměnné1;
              Proměnná2 : TypProměnné2;
              ...
             end;

Konkrétní případ:

type TPohlavi = (muz,zena);
TPracovnik = record
             Jmeno  : string;
             Pohlavi: TPohlavi;
             Vek    : Byte; {Vek pracujiciho urcite lezi mezi 0 a 128roky)
             Zenat  : Boolean;
             Prijem : Word; {Zaporne platy nedavame, ale obcas jsme nad 32 000}
            end;

Používání tohoto typu poněkud zvyšuje přehlednost programu. (ale nezapomínejte na základní pravidla...). Podívejme se nyní na práci se záznamy. Použijme k tomu Frantu, našeho pracovníka:

var Franta1,Franta2 : TPracovnik;
begin
  Franta1.Jmeno := 'Franta';
  Franta1.Pohlavi := muz;
  Franta1.Vek:=18;
  Franta1.Zenat:=True;
  Franta1.Prijem:=150;
  Franta2:=Franta1;
 writeln(Franta2.Prijem);
 readln;
end.

Proměnné typu záznam lze tedy navzájem přiřazovat (jedná-li se o stejný typ záznamu). Ukázali jsme si také přístup k jednotlivým položkám záznamu. Neustále vypisování Franta1. je ovšem poněkud těžkopádné. Proto byl zaveden příkaz with, s jehož využitím náš program vypadá takto:

...
begin
with Franta1 do
 begin
 Jmeno:='Franta';
 Pohlavi:=muz;
 Vek:=18;
 Zenat:=True;
 Prijem:=200;
end;
Franta2:=Franta1;
...
end.

Použití příkazu with by mělo být z uvedené ukázky zřejmé.
Pokud chcete, podívejte se do tabulky v první lekci, kolik již znáte klíčových slov.

Soubor

Tento typ se poněkud vymyká z ostatních struktur. Nesmí být součástí žádné jiné struktury.
Nesmí obsahovat ukazatele. (Ukazatel ukazuje na určité místo v paměti a není zaručeno, že při dalším spuštění bude proměnná na tom samém místě).

Pascal rozlišuje tři typy souborů: textový soubor (data ukládána jako text), soubor s udaným typem (data ukládána binárně, nejde se v nich vyznat a nejdou přenést na jiný počítač), soubor bez udaného typy (do souboru zapsán obsah paměti).

Nejjednodušším a nejpřenositelnějším typem je textový soubor Text;

Podívejme se na to, jak se se souborem zachází.
Nejdříve musíme oznámit, s jaké je skutečné jméno souboru, se kterým pracujeme. To provedeme pomocí Assign(soubor, jméno_v_Dosu);

Teď se musíme rozhodnout, co vlastně se souborem chceme dělat. Chceme-li do něj zapisovat, použijeme proceduru
Rewrite(soubor); - původní soubor se vymaže a je založen nový; anebo
Append(soubor); - data se připojí na konec souboru

Chceme-li ze souboru data získat, použijeme Reset(Soubor);

Až přestaneme se souborem pracovat, nesmíme zapomenout soubor uzavřít pomocí Close(Soubor);

Raději si to ukažme na příkladu :

program Telefonni_seznam;
var fsoubor : Text; {Jména proměnných typu soubor obvykle začínají na f.}
begin
 Assign(fsoubor,'c:\seznam.dat');
 Rewrite(fsoubor);
 writeln('Teď můžeme do souboru zapisovat');
 Close(fsoubor);
 writeln('Ted jsem soubor zavřeli a nic nemůžeme');
 Reset(fsoubor);{Ted muzeme ze souboru cist}
 Close(fsoubor);
 Readln; {A končíme}
end.

Jak se takový soubor používá? K tomu slouží procedury, které už známe - write, writeln, read, readln.
write(soubor, proměnná) - zapíše do souboru proměnnou (ta musí mít správný typ)
writeln(soubor, proměnná) - totéž, ale přidá znak konce řádky
read(soubor, proměnná) - přečte ze souboru hodnotu a uloží ji do proměnné
readln(soubor, proměnná) - přečte hodnotu a přejde na další řádek souboru

Otevřeme-li soubor pro čtení, nastaví se na první znak v souboru pomyslný ukazatel. Pokud přečteme proměnnou typu string, načte se celá řádka a ukazatel skočí na druhou. Pokud načteme číslo, program vynechá všechny mezery a pokusí se načíst číslo (pokud následuje znak, skončí chybou), pokud přečteme char program přečte jeden znak a posune ukazatel na další (pozor i znak konce řádky je znak). Načítáme-li data ze souboru, potřebujeme zjistit, zda náhodou nejsme na konci. K tomu slouží následující funkce:
Eoln(Soubor):Boolean - Jsme v Souboru na konci řádky? Eof:Boolean; - Je konec souboru?
Eof(Soubor):Boolean; - Je v souboru Soubor konec souboru?

Raději si to vyzkoušejme na příkladu. Tentokrát bude poněkud delší a úplný. Tak nějak by programy měly vypadat.

Program Data;
var Soubor : Text;
           Z      : char;
           Jmeno  : string;
           Telefon: LongInt;

procedure Zapis;
begin
 Append(Soubor);
 repeat
  Write('Jmeno : ');
  Readln(Jmeno);
  if Jmeno<>'' then
   begin
    Write('Telefon : ');
    Readln(Telefon);
    Writeln(Soubor, Jmeno);
    Writeln(Soubor, Telefon);
    Writeln(Soubor); {Do souboru zapise prazdnou radku, je zde kvuli prehlednosti ukladanych dat}
   end;
  until Jmeno='';
 Close(Soubor);
end;

procedure Prepis;
begin
 Rewrite(Soubor);
 repeat
  Write('Jmeno : ');
  Readln(Jmeno);
  if Jmeno<>'' then
   begin
    Write('Telefon : ');
    Readln(Telefon);
    Writeln(Soubor,Jmeno);
    Writeln(Soubor,Telefon);
    Writeln(Soubor); {Do souboru zapise prazdnou radku kvuli prehlednosti}
   end;
 until Jmeno='';
 Close(Soubor);
end;

procedure Cteni;
begin
 Reset(Soubor);
 repeat
  Readln(Soubor,Jmeno);
  Readln(Soubor,Telefon);
  Readln(Soubor); {Preskoci prazdnou radku}
  Writeln(Telefon:9,'     ',Jmeno);
 until Eof(Soubor);
 Close(Soubor);
 Readln;
end;

begin
 Assign(Soubor,'Cisla.txt');
 Writeln('Co chcete dělat P-přepis, Z-zápis či T-čtení');
 repeat
  Readln(Z);
  Z:=UpCase(Z);
  if Z = 'Z' then Zapis;
  if Z = 'P' then Prepis;
  if Z = 'T' then Cteni;
until ((Z='Z')or (Z<>'T') or (Z<>'P'));
end.

Program přepište a několikrát spusťte, pokaždé vyzkoušejte jiný způsob přístupu k souboru (čtení, zápis, čtení, zápis, čtení, přepis, čtení...) Podívejte se, jak se mění soubor "Cisla.txt".
Pozorně si příklad projděte a snažte se každý příkaz odůvodnit či vylepšit. (Je tam write, protože vypadá lépe, zadáváme-li data na stejný řádek. Program je členěn do procedur kvůli přehlednosti, zde by bylo lepší udělat samostatnou proceduru na zápis - a měnit jen styl otevření souboru...).

To by bylo pro dnešek vše.
DCV: Důkladně si zopakujte ukazatele a záznamy. Napište několik programů na využití struktur. Zkoušejte i různé kombinace struktur (záznam obsahující pole, záznam obsahující dvourozměrné pole a množinu, soubor obsahující tento záznam...) Sledujte velikost proměnných v paměti (využijte SizeOf();).

Zopakujte si metodu návrhu programu shora dolů a první lekce pro pokročilé.