Szabványos PASCAL
Programozás és algoritmusok

Dr. Pongor György, 1989

Tartalom

Bevezető
1. Ismerkedés a PASCAL nyelvvel és a programkészítéssel
1.1. Az első program
1.2. Az első program jelentése
1.3. Szintaxis és szemantika: forma és tartalom
1.4. Szintaxisábra
1.5. Folyamatábra
1.6. Szabályok leírási módja
1.7. A repeat utasítás
1.8. Az egész (integer) típus
1.9. Példák
1.10. A szabványról

2. Algoritmusok egész, valós logikai és karakteres adatokkal
2.1. A valós (real) típus
2.2 Az if utasítás
2.3. Függvények használata
2.4. Az utasításcsoport: begin...end
2.5. Összetett logikai feltétel: and, or és not művelet
2.6. A logikai (boolean) típus
2.7. A while utasítás
2.8. Egy ravasz feladat
2.9. Egyenlet numerikus megoldása
2.10. Határozott integrál numerikus meghatározása, a for utasítás
2.11. A karakter (char) típus, a case utasítás
2.12. Konstans definiálása
2.13. Az adatbeolvasás és adatkiírás elemei

3. További adattípusok, típusdefiníció
3.1. Egyindexű tömbök
3.2. Többindexű tömbök
3.3. Típus definiálása: type
3.4. Tömbalgoritmusok
3.5. Intervallum típus
3.6. Szabványos típusok átdefiniálása, adatok kompatibilitása
3.7. A karakterlánc (string) típus
3.8. Rekord típus
3.9. A with utasítás
3.10. Táblázatkezelési módszerek
3.11. Változók deklarálása
3.12. Pakolt adattípusok


4. Szegmensek: Eljárások, Függvények
4.1. A szegmentálás fogalma
4.2. Lokális és globális változók, azonosítók hatásköre
4.3. Adatátadás a szegmens és a hívó programrész között
4.4. A szegmensek mint paraméterek
4.5 Rekurzió

5. Mutató típus, dinamikus változó
5.1. A dinamikus változó fogalma
5.2. Tipikus adatszerkezetek dinamikus adatokkal

6. További adattípusok
6.1. Felsorolt típus
6.2. A halmaz (set) típus
6.3. Az adatállomány (file) típus

7. További szabályok
7.1. Program
7.2. A goto utasítás
7.3. Az üres utasítás
7.4. Szimbólumelválasztók
7.5. Megjegyzés (komment)
7.6. Helyettesítő szimbólumok
7.7. Az adattípusok összefoglalása

Irodalomjegyzék

Bevezető

"És mindig csak kívánságról kívánságra haladhatsz.
Amit nem kívánsz, az elérhetetlen a számodra."
[0] 234. oldal

Ha a Kedves Olvasó a kezébe vette, bizonyára nem véletlenül választott programozási nyelvről, és éppen a Pascalról szóló könyvet.
Ezért nem untatom a gépi adatfeldolgozás vagy általában a számítástechnika jelentőségének ecsetelésével, sem azzal, hogy bevezetőt tartsak a számítógépek működéséről.
A Pascal úgynevezett magas szintű programozási nyelv, használatához nem is kell ismerni a számítógép működését. Nem szükséges ismernünk azt az egyébként nagyon érdekes folyamatot sem, ahogyan a Pascal nyelven megírt programból a gép által végrehajtható kód lesz.

Érdekes azonban, hogy hogyan lehet Pascal nyelven olyan programokat írni, amelyek megoldják a rájuk bízott feladatot.
Miért éppen Pascalban programozzunk? Mi teszi különösen érdekessé és egyre elterjedtebbé ezt a programozási nyelvet, amikor annyi hatékony és érdekes nyelvet találtak már ki, és annyi programot megírtak már az eddig kidolgozott és világszerte elterjedt programozási nyelveken? Ha életünk egy hosszabb-rövidebb részét arra szenteljük, hogy megtanuljuk ezt a nyelvet és mindennapi munkánk során ezen fogalmazzuk meg gondolatainkat egy információfeldolgozó gép számára, akkor sok tulajdonságát lehet és kell megvizsgálni. Néhány szempont egy programozási nyelv kiválasztásához:

... és még sorolhatnánk tovább.
Van egyáltalán olyan programozási nyelv, amely mindezeket egyszerre teljesíti? Természetesen nincs. Nem is lehet, hiszen ezek a szempontok részben vagy teljesen ellentmondóak. Nézzük például az első két szempontot! Ha egy nyelvnek gazdag az utasításkészlete, akkor azt biztosan nem könnyű megtanulni. Példa erre a PL/1 nyelv, amelynek nagyon sok lehetősége van, de ezek közül egy adott problémához csak keveset használunk. Másrészt egy olyan összetett lehetőségeket is tartalmazó nyelvhez, mint a PL/1, nem lehet kicsi és gyors fordítóprogramot írni. A megfelelő nyelv kiválasztása tehát mindig valamilyen kompromisszum eredménye.

Niklaus Wirth, a Zürichi Műszaki Egyetem professzora a Pascal nyelvet oktatási célból tervezte. Olyan nyelvet akart definiálni, amely könnyen tanulható, jó programozási szokásokat sugall, ill. támogat, és kis méretű számítógépeken is jól megvalósítható. A nyelv első alakját 1968-ban fogalmazta meg, az első Pascal fordítóprogram 1970-ben készült el, a nyelv első, szélesebb körben is ismert leírása pedig 1972-ben jelent meg. Ezt magyar nyelven a Műszaki Könyvkiadó jelentette meg 1982-ben "A Pascal programozási nyelv (Felhasználói kézikönyv és a nyelv formális leírása)" címmel.
A nyelv gyors elterjedése és az összegyűlt tapasztalatok 1982-ben a nemzetközi szabvány megalkotásához vezettek. Ez sok kisebb és néhány lényeges dologban eltér az "eredeti", Wirth által leírt nyelvtől.

E könyv első kiadása volt az első magyar nyelven megjelent leírás, amely a szabványos nyelvet tárgyalja. A Pascal nyelv szabványa azóta megjelent Kathleen Jensen, Niklaus Wirth: "A Pascal programozási nyelv, Felhasználói kézikönyv és a nyelv formális leírása (az ISO Pascal szabvány szerint)" című, a Műszaki Könyvkiadó gondozásában kiadott könyvben is.
Ez a könyv nem egyszerűen a szabvány fordítása, a szabvány fordítását eredeti formájában nem is tartalmazza. Aki olvasott már szabványleírást, az tapasztalhatta, hogy nem éppen könnyű olvasmány, hiszen egy szabvány megfogalmazásakor mindenekelőtt a pontosságra, az egyértelműségre és a tömörségre törekednek, A szabvány szövege szinte tanulhatatlan: minden tulajdonságot és fogalmat csak egyszer ad meg, a többi előfordulási helyen legfeljebb hivatkozik a definiálás helyére.

Könyvünk ezért mindazt az információt tartalmazza, amit a Pascal nyelv szabványa. Az anyag csoportosítása azonban elsősorban didaktikai szempontok szerint történt. Az egyes nyelvi elemek és szabályok ismertetése dőlten szedett, és általában lényegesen részletesebb, mint a szabvány. Ez is segíti az Olvasót, ha a nyelvet ennek a könyvnek a segítségével szeretné megismerni, vagy ha a gyakorlati programozási munka során ezt a könyvet szeretné kézikönyvként használni.
Arra is törekedtem, hogy egy-egy nyelvi eszköz tárgyalásakor egy helyen adjam meg az összes olyan szabályt, amely rá vonatkozik. Ennek kettős célja volt. Egyrészt így sokkal könnyebb kézikönyvként is használni ezt a kiadványt, hiszen nem kell az adott elem különböző vonatkozásait a könyv különböző pontjairól "összevadászni". Másrészt félreértések adódnának abból, ha egy elem bemutatásakor az előismeretek hiányára hivatkozva csak félig-meddig mutatnék be valamit, majd később, esetleg ettől távol, sőt több helyen is további szabályokkal egészíteném ki a fogalomról szerzett ismereteket.

A nyelvi eszközök tulajdonságainak ilyen koncentrált tárgyalása két következménnyel jár. Célszerű néhány tulajdonságot több helyen is megemlíteni, hiszen az egyes fogalmak szorosan kapcsolódnak egymáshoz, Így sok tulajdonság több nyelvi elemet is érint, esetleg éppen azok kapcsolatát jellemzi. Ilyen esetekben az adott tulajdonság leírása megismétlődik, ez megszabadít az esetleges visszakeresésektől, a gyakorlati programozás közben elkövetett hiba okának hosszadalmas nyomozásától, de az ismétlés, emlékeztetés el is mélyíti a már megszerzett tudást.
A másik következmény, hogy az Olvasó már az első ismerkedéskor találkozik olyan fogalmakkal, amelyek részletes tárgyalása csak később következik. Ez elkerülhetetlen, mert egy fogalom pontos magyarázatához szükség van olyan más fogalmak használatára is, amelyek közvetlenül vagy közvetve éppen rá hivatkoznak. Ezt a bűvös kört valahol meg kellett szakítani, azaz kénytelenek vagyunk bizonyos helyeken addig még nem tárgyalt fogalmakat is használni. Természetesen igyekeztem az anyagot úgy elrendezni, hogy az ilyen előrehivatkozások minél ritkábban forduljanak elő. Ha az Olvasót mégis zavarná, nyugodtan átugorhatja azokat a részeket, amelyek ilyen, még nem tárgyalt fogalmakat tartalmaznak. A hivatkozott fogalom tárgyalásakor majd visszatérhet a kihagyott tulajdonságokra.
A könyvben példák és feladatok is szerepelnek, a példák megoldása pedig részletes magyarázattal együtt. A feladatok, némi gyakorlási lehetőséget nyújtva, az önálló munkát segítik. (A példákban általában kitérünk a Turbo Pascal és HiSoft-Pascal megoldásokra.) Javasolható a példák gondos tanulmányozása, probléma esetén pedig az előtte leírtak ismétlése. A feladatok megoldását érdemes azonnal gépen is kipróbálni, mert a csak papíron létező programok a valóságban sokszor - hogy finoman fogalmazzunk - nem az elképzelésünknek megfelelően működnek.
A bemutatott programokat és programrészleteket úgy igyekeztem megválasztani, hogy azok tipikus programozási feladatokat, illetve a nyelvi elemek tipikus alkalmazását mutassák be. Akiket további érdekes programok és programkészítési módszerek is érdekelnek, azok figyelmébe ajánlom az irodalomjegyzékben megadott könyveket.

Értékes megjegyzésekkel és javaslatokkal sok barátom és kollégám segített a könyv megírásában, így köszönet illeti Kapocska Zsuzsát, Nemes Mihályt, Csopaki Gyulát, Pápay Zsoltot, Horváth Lászlót és Győri Jenőt.
Külön megköszönöm Göller Gabriellának és Várszeginé Burán Zsuzsának az alapos és lelkiismeretes szerkesztői munkát.
Végül a könyv talán legalaposabb és legkritikusabb lektora az a többezer hallgató volt, aki a Budapesti Műszaki Egyetem Villamosmérnöki és Informatikai Karán az előző kiadásokat tankönyvként használta. Számos apróbb-nagyobb javítást végeztem a könyvön észrevételeik alapján, melyeket most köszönök meg.

Tudom, hogy egyetlen könyv sem lehet tökéletes, bármennyire törekszik is rá a szerző. Kimarad sok érdekes megoldás, fontos algoritmus. És bizony maradnak még hibák is benne. Ha Ön, kedves Olvasó, hibára bukkanna, vagy bármilyen javaslata van a könyv tartalmára vonatkozólag, azt a további kiadásokban igyekszem figyelembe venni, ezért arra kérem, tájékoztasson róla.

1. Ismerkedés a PASCAL nyelvvel és a programkészítéssel

Kezdjük a Pascal nyelv megismerését a programok legfontosabb elemeinek áttekintésével! Ezt egy rövid programon keresztül tesszük, közben találkozunk a programkészítés és programírás legfontosabb fogalmaival. Az ismerkedés rövidebb, tömörebb is lehetne, mint ez a fejezet, a Pascal nyelvvel először találkozó Olvasónak azonban érdemes részletesen átgondolni az alapfogalmakat.
Mindjárt a könyv elején megjegyezzük, hogy a számítástechnikai szokások miatt, az egyértelmű jelölés érdekében vétenünk kellett a helyesírás szabályai ellen, a szöveges részekben is pontot tettünk tizedesvessző helyett. Szintén a félreértések elkerülése érdekében használtuk az (a, b) intervallum leírására az a...b jelölést (de nem két ponttal, mint a Pascal programokban), ebben- az esetben ugyanis a zárójelek okoztak volna gondot.

1.1. Az első program

1.1.1. Példa:
Készítsünk programot n!- azaz n faktoriálisának - kiszámítására és kiírására. Legyen n a program bemeneti adata!

Megoldás:
A faktoriális matematikai fogalom: egy n egész szám faktoriálisát az első n pozitív egész szám szorzata adja meg, képlettel:

n! = 1*2*3*...*(n-2)*(n-1)-n

A * (csillag) jelet itt a szorzás műveletének jelzésére használtuk.

A programot úgy szeretnénk megírni, hogy ne csak egy adott szám faktoriálisát tudja kiszámítani, hanem le-hetőleg bármilyen megadott számét. Ez azt jelenti, hogy amikor a program elkezdi működését, a program használójától valahogyan meg kell tudnia, hogy melyik egész szám faktoriálisát kell éppen meghatározni. Ilyenkor általában azt a módszert szoktuk használni, hogy a számítás előtt a program megkérdezi ezt a számot, majd megvárja, amíg a felhasználó a számítógép billentyűzetén beírja. Ezt a beírt számot azután a program valamilyen módon beolvassa, így most már ismeri n értékét. Azt mondjuk, hogy n a program bemeneti adata. A programnak van kimeneti adata is, mégpedig az a szám vagy egyéb adat, amit a program kiszámol, azaz a program eredménye. A mi programunk kimeneti adata a beolvasott szám faktoriálisa lesz.
Természetesen egy programnak lehet több, esetleg nagyon sok és sokféle bemeneti, illetve kimeneti adata. Egy névsorrendező programnak például a rendezetlen névsor a bemeneti adata. Ez lehet, hogy csak két névből áll, de az is lehet, hogy kétezerből. Kimeneti adata pedig ugyanez a névsor, ábécérendbe szedve. Az eddigiek alapján első programunk általános szerkezetét így tudnánk vázlatosan megadni:

n beolvasása
faktoriális kiszámítása
faktoriális kiírása

Ez a vázlat lényegében három lépést, úgy is mondhatjuk, hogy három utasítást tartalmaz: azokat az utasításokat, amelyeket a programnak végre kell hajtania. Egy program ugyanis utasítások sorozata, melyeket a számítógép valamilyen módon végre tud hajtani. A gép által végrehajtható utasítások természetesen nem magyar nyelvű mondatok. Az adott programozási nyelvre éppen az a jellemző, hogy az utasításokat milyen formában adjuk meg, illetve, hogy egyáltalán milyen utasítások vannak az adott nyelvben, és mi ezeknek az utasításoknak a jelentése, azaz mi a végrehajtásuk módja.

Még nem ismerjük ugyan a Pascal nyelvet, mégis sejthetjük, hogy van olyan utasítása, amely beolvas egy egész számot, sőt olyan is, amely kiír egyet. Így előbbi vázlatunk első és utolsó sorának pontosan egy-egy Pascal nyelven írott utasítás felel meg.
Olyan utasítás azonban, amely egy szám faktoriálisát kiszámítaná, ebben a nyelvben nincs. Ez furcsának tűnhet, de az az oka, hogy a programokban viszonylag ritkán van szükség a faktoriális kiszámítására. Ezért a nyelv megalkotói nem tartottak szükségesnek egy ilyen utasítást, ugyanúgy, ahogyan például sok zsebszámológép sem tud faktoriálist számolni.
Nem tehetünk mást, össze kell szorozni az egymást követő egész értékeket, amíg a szorzó elég nagy nem lesz, azaz el nem éri n értékét, mintha papírral, ceruzával, azaz mindenféle gépi segédeszköz vagy táblázat nélkül kellene ezt a számítást elvégeznünk. Szerencsére a szorzás és az összeadás már nagyon gyakran használt művelet, a programozási nyelvekben ezeket közvetlenül használhatjuk.
Fogalmazzuk meg a faktoriális kiszámításának ezt a módszerét az előbb látott programvázlathoz hasonló módon!

Faktoriális kiszámítása:

n beolvasása
legyen a faktoriális és a szorzó értéke is kezdetben 1
ismételjük a következő műveleteket:
   legyen a faktoriális új értéke a régi érték és a
      szorzó szorzata,
   legyen a szorzó új értéke a régi érték +1
fejezzük be az előbbiek ismételt végrehajtását, ha
   a szorzó értéke nagyobb lett, mint n
faktoriális kiírása

Ezzel megalkottuk a faktoriális kiszámításának módszerét. Gyakran használjuk az "algoritmus" szót a számítási, adatfeldolgozási módszer szinonimájaként. Az algoritmus olyan módszert jelent, amely véges számú lépésben elvezet egy feladat megoldásához.
Ez már elég részletes terv ahhoz, hogy elkészítsük (megírjuk) a megfelelő programot valamilyen programozási nyelven.

Sok különböző programozási nyelv létezik, és ha az előbbi tervnek megfelelő programot ezeken megírjuk, akkor különböző szövegű, de azonos működésű programokat kapunk. Még röviden sem szeretnénk a különböző programozási nyelvek összehasonlításával foglalkozni, csupán bemutatjuk, hogy ugyanannak a feladatnak több különböző program is megfelelhet. A fenti tervnek megfelelő program BASIC, C, majd Pascal nyelven a következőképpen néz ki:

BASIC nyelvű program n! kiszámítására:

10 INPUT N
20 LET F=1
30 FOR SZ=2 TO N
40   LET F=F*SZ
50 NEXT F
"Ez azonban már más történet,
és elbeszélésére más alkalommal kerül majd sor."

[0] 278. oldal
60 PRINT N;"faktorialis =";F

C nyelvű program n! kiszámítására:

main()
{
  int n,faktorialis,szorzo;
  scanf("%d",&n);
  faktorialis=szorzo=1;
  do
    faktorialis*=szorzo++;
  while(szorzo<=n);
  printf("%d faktorialis = %d /n", n, faktorialis);
}

Pascal nyelvű program n! kiszámítására:

program Elso;
{ Adott szam faktorialisanak kiszamitasa }
var n,faktorialis,szorzo:integer;
begin
  Read(n); { n beolvasasa }
  faktorialis:=1;szorzo:=1; { kezdeti ertekek }
  repeat { ciklus }
    faktorialis:=faktorialis*szorzo;
    szorzo:=szorzo+1; { kovetkezo reszsorozat }
  until szorzo>n;
  Write(n,' faktorialis = ',faktorialis)
end.

Turbo Pascal-ban Read(n) helyett Readln(n) használandó!

A Pascal nyelv megismerését kezdjük azzal, hogy sorra bemutatjuk e program egyes elemeit. Nem feltétlenül a program sorai szerint haladunk, hanem olyan sorrendben, hogy a következő új fogalmat az előzőek alapján a lehető legkönnyebben meg lehessen érteni.
Az első szembetűnő dolog, hogy a programban sok angol szóval találkozunk. Ez nemcsak a Pascal nyelvre, hanem általában a programozási nyelvekre jellemző, ugyanis a számítógépek, és így a programozási nyelvek használata először angol nyelvterületen terjedt el. Egy kicsit könnyebb dolga van annak, aki tud angolul, hiszen a Pascalban használt angol szavak jelentése hasonló az itt érvényes jelentésükhöz. Aki nem tud angolul, annak sem kell megijednie: csak pár tucat ilyen szóval találkozunk, ezeket könnyen meg lehet jegyezni.
A programban a szavakon és egyéb betűkön kívül számjegyeket és további speciális jeleket is találunk, például * ( ) ; : := ' .


A Pascal nyelv karakterkészlete:
A számítástechnikában karakternek hívjuk a betűket, a számjegyeket és egyéb, ún. speciális jeleket.

Betűk: A B C D E F G H I J K L M N O P Q R S T U V W K Y Z
  a b c d e f g h i j k l m n o p q r s t u v w x y z

tehát az angol ábécé betűi.

A kis- és nagybetűket a gép azonosaknak tekinti, kivéve a programozó által szöveghűen előírt adatok, azaz a karakterek és az úgynevezett karakterláncok esetében. Így PRoGRAM, proGrAm és Program ugyanazt jelenti, de 'a' és 'A' különbözik egymástól. (Ez alól csak nagyon kevés implementáció kivétel, pl. HiSoft-Pascal hivatalosan kiadott Spectrum és Enterprise verziói megkülönbözteti a kis- és a nagybetűket így a foglalt szavakat, előre definiált azonosítókat csak nagybetűvel írhatjuk.)

Számjegyek: 0 1 2 3 4 5 6 7 8 9
Speciális karakterek: + - * / = < > [ ] . , : ; _ ( ) { } '
  szóköz (angolul: space), valamint az adott megvalósításban használható további karakterek.

(A HiSoft-Pascal az '_' (aláhúzás) karakter használatát nem támogatja, így azt nem használhatjuk azonosítókban, csak szövegfüzérekben.)

A karakterek tehát általában megfelelnek a számítógépek adatbeviteli és kiírási eszközein, pl. billentyűzeten, nyomtatón, képernyős megjelenítőn alkalmazott karakterkészletnek.
Sok gépen nem lehet a magyar helyesírásnak megfelelő ékezetes magánhangzókat használni. A mintaprogramokban elkerültük ezeket a betűket, így az Olvasó minden érdemi változtatás nélkül begépelheti és kipróbálhatja a példákat.
A karakterekből szavak, további összetett speciális kifejezések, ezekből pedig sorok állíthatók össze. A program ezekből a sorokból áll.

Kulcsszó, azaz foglalt szó
Minden Pascal nyelvű program a program szóval kezdődik (de a Turbo Pascal-bvan elhagyható). Ez a szó, több mással együtt, a Pascal nyelvben úgynevezett kulcsszó, más néven szószimbólum. A kulcsszavak legfontosabb szerepe, hogy megmutatják a program részeit, szerkezetét, legfontosabb elemeit. A kulcsszavakat foglalt szavaknak is nevezik, mivel ezeket csak egy bizonyos értelemben, mégpedig a program szerkezetének megadására, illetve adott műveletek leírására használhatjuk, vagyis már foglaltak e célra.
A faktoriális számító programban használt kulcsszavak: program, var, begin, repeat, untul, end
Mivel a kulcsszavak írják le a Pascal programok szerkezetét, alapvető jelentőségűek.


A szabványos Pascal nyelv kulcsszavai:

and array begin case const
div do downto else end
file for function goto if
in label mod nil not
of or packed procedue program
record repeat set then to
to type until var while
with        

A kulcsszavakat mindig egybe kell írni, tehát nem lehet bennük szóköz és nem választhatók el (azaz nem folytathatók új sorban sem). Így például goto helyett nem írható go to, bár az angol nyelvben így lenne helyes.


Ugyanezek a szabályok vonatkoznak egy másik nyelvi elemre, a direktívára is. A szabványos Pascal nyelv egyetlen direktívát ismer, ez a forward, melyet az eljárások és függvények tárgyalásakor ismertetünk.

Ez egyes megvalósításokban több-kevesebb kulcsszó lehet:

Azonosító, azaz név
A faktoriális számító program 1. sorára visszatérve, a program szó után következik az elso szó, ez a program neve. Nincs jelentősége annak, hogy a programunknak milyen nevet adunk, mégis érdemes a tényleges feladatot jellemző nevet választani. Így később, ha kezünkbe kerül a program szövege, általában már a neve emlékeztet arra, hogy milyen feladatot old meg. A név, más szóval azonosító, a programozás nyelvek egyik legfontosabb fogalma. A programok különböző elemeit, például adatait, az egyes programrészeket, az általunk létrehozott adattípusokat névvel látjuk el, és ezzel a névvel lehet hivatkozni rájuk.
A faktoriális számító programban szereplő azonosítók: elso, n, faktorialis, szorzo, integer, read, write

Egy név mindig betűvel kezdődik és betűkkel vagy számjegyekkel lehet folytatni. Tetszőlegesen hosszú lehet és természetesen legalább egy betűből áll. Nem lehet benne a betűkön és számjegyeken kívül más karakter, így szóköz sem, és nem folytatható új sorban.
A szabványos Pascal nyelvben a nevek minden karaktere fontos, azaz ha két név csak egy karakterben is eltér, már különbözőnek tekintjük őket. Nagyon gyakori azonban, hogy egy gép, pontosabban az a program, amely az adott gépen Pascal programok futtatását lehetővé teszi, nem vizsgálja meg a nevek minden karakterét, hanem például csak az első nyolcat, a HiSoft-Passcal az első tízet (Turbo Pascal-ban maximum 127 karakter hosszú lehet egy azonosító) a többit figyelmen kívül hagyja. Így az a három név, hogy Budapest, Budapestre, budapesti a szabványos Pascalban különbözőnek, valamely implementációban viszont azonosnak minősül. Ezért, ha nem akarunk kellemetlen meglepetéseket, érdemes már az első nyolc karakteren megkülönböztetni a különböző azonosítókat, már csak a jobb olvashatóság érdekében is.

Szabályos nevek pl.: A alma AbCdefgh ijkLmN Cl Qkac kl34692z8 ml6F4

Hibás nevek pl.: 1A (nem betűvel kezdődik, hanem számjeggyel)
M!2 (a ! nem betű és nem számjegy)
IN (ez kulcsszó, tehát nem használhatunk ilyen azonosítót)

Mint már említettük, az azonosítók sokféle dolgot jelenthetnek, egy adott azonosító azonban mindig pontosan egy dolgot azonosít; éppen az a feladata, hogy az adott nyelvi elemet megkülönböztető névvel lássa el.
Az első programban például az elso a teljes programnak a neve; az n, a faktorialis és a szorzo egy-egy adat tárolására használatos nyelvi elemnek, az úgynevezett változónak a neve; a read és a write pedig eljárásnév.

A nevekre vonatkozó legfontosabb szabályokat már ismerjük, a névvel jelzett dolgok jellemzőit a későbbiekben, lépésenként ismerjük meg.
A továbbiakban azt a két szót, hogy "név" és "azonosító" teljesen azonos értelemben, egymás pontos szinonimájaként használjuk.

Számok
Első programunkban csak két helyen találunk számot, mindkettő az 1, jelentésük teljesen nyilvánvaló.
A számokat a Pascal nyelvben tízes számrendszerben adjuk meg, nem kettesben, ahogy a gép belül dolgozik. Egy magas szintű nyelvnek, mint a Pascal, ez is előnye: nem nagyon kell törődnünk a számok belső, azaz gépi ábrázolásával, a programban és a bemeneti adatok megadásakor a megszokott decimális, azaz tízes számrendszerbeli számokat használhatjuk (bár a legtöbb megvalósításban - így a Turbo Pascal-ban és HiSoft-Pascalban is - megadhatjuk a számokat hexadecimális alakban is), az eredményt a gép is tízes számrendszerben írja ki.

Az első programokban csak a legegyszerűbb számokkal, az egészekkel találkozunk. Ezek egy vagy több, 0-tól 9-ig terjedő számjegyből állnak. Használhatjuk a + illetve - előjelet is, a pozitív előjel elhagyható. Egy szám számjegyeit, illetve a számot és előjelét semmi sem választhatja el egymástól, a számot megadó karaktersorozat végét éppen az jelzi, hogy olyan karakter következik, amely nem számjegy.

Helyes egész számok például: 1  12  +384  -61  0  012

Hibás egész számok pl.: 123 456 (szóköz van benne)
  33+44 (+ jel van benne, így nem egy egész szám, hanem két egész szám összege)
  +-31 (csak egy előjel lehet)
  - 66 (szóköz van az előjel és a szám többi része között)
  5,6 (vessző van benne)
  5a6 (betű van benne)
  5E6 (e a tizedes kitevő jele)
  5.6 (pont van benne)

Az utóbbi kettő jó valós szám lenne, de mi mist az egészekkel foglalkozunk.

Egy egész szám elvileg tetszőlegesen nagy is lehetne. A nagyon nagy, tehát például sok számjegyből álló számok tárolásához azonban nagy tárra lenne szükség, és a számítási műveletek végrehajtása, például egy szorzás elvégzése is hosszadalmas lenne. Ezért minden gépnél van egy érték, amelynél nagyobb egész számot a gép közvetlenül nem tud kezelni. Ennek az értéknek a Pascal nyelvben neve, azaz azonosítója is van: maxint. Ez a MAXimal INTeger rövidítése, magyarul: legnagyobb egész.
Az adott Pascal rendszer kézikönyve mindig megadja ennek a számértékét is, a Turbo Pascalban és HiSoft-Pascalban például
maxint=32767

Speciális szimbólumok
Első programunkban további karaktereket is találunk, melyek nem is betűk, nem is számjegyek. Ezeket speciális karaktereknek nevezzük, a speciális szimbólumok pedig egy vagy két speciális karakterből állhatnak, sőt a kulcsszavakat is speciális szimbólumnak tekintjük.


Speciális szimbólumok: + - * / = < > [ ] { } ( ) . , : ; *
<> <= >= := ..
  és a kulcsszavak

A felsorolás második sorában találhatók kétkarakteres speciális szimbólumok is. Ezeknél a két karakter között nem lehet semmilyen más karakter, így szóköz sem, nem is választhatók el új sorba.

A speciális szimbólumok egy részének a matematikában megszokott jelentése van, így például a - jel negatív előjelet vagy kivonást jelent, más részük olyan jelet ír le, amelyet egy karakteren nem tudunk kifejezni, például a <=, ami kisebb vagy egyenlőt jelent. Az egyik leggyakrabban használt szimbólum a :=, ez értékadást jelent. Olyan utasítást adunk meg a segítségével, amely beállítja vagy megváltoztatja valaminek, például egy adatnak az értékét. Úgy olvassuk ki, hogy "legyen egyenlő".

Megjegyzések: kommentek
Mintaprogramunkban a sorok jobb oldalán kapcsos zárójelek között rövid magyarázatokat találunk.
Egy Pascal programban kapcsos zárójelek közé tetszőleges karaktersorozatokat írhatunk, kivéve természetesen a jobb oldali kapcsos zárójelet. A { és } közötti szöveget a program többi szövegéhez hasonlóan a gép beolvassa, tárolja, kiírja, de a program tevékenységére nincs hatással. Ezeket megjegyzéseknek, vagy az angol megfelelő alapján kommenteknek nevezzük. A megjegyzésekre vonatkozó szabályokat a 7.5. fejezetben ismertetjük.

Felvetődhet a kérdés: miért írunk a programba olyan szöveget, amelyet a gép nem vesz figyelembe, azaz olyan, mintha ott sem lenne?
Nos, a gép számára valóban mindegy, hogy vannak-e kommentek. Még egy kicsit rossz is, mert elfoglalnak valamennyi helyet a memóriában illetve azon az adattárolón, például mágneslemezen, ahol a program szövegét tároljuk. A komment azonban nem a gépnek szól! Nekünk, akik írjuk és a program fejlesztése közben vagy később elolvassuk a programot, egyáltalán nem lényegtelen, hogy milyen könnyen emlékezünk vissza az adott utasítás céljára. Ez különösen így van, ha más írta a programot, amelyet meg kell értenünk.
A megjegyzés tehát a program szövegének olvasójához szól; megmagyarázza, kommentálja a programot. Ami rövid és egyszerű mintaprogramunknál még feleslegesnek tűnhet az alapos magyarázat. Gondoljunk azonban arra, hogy egy valamirevaló program akár néhány ezer soros is lehet, és amikor annak valamelyik részén dolgozunk, már biztosan nem fogunk emlékezni, pontosan mit is jelentett egy g:=q+l utasítás néhány ezer sorral előbb. Igaz, az utasítás környezetéből több-kevesebb munkával ki lehet deríteni mindennek a jelentését, de teljesen felesleges ilyen nyomozásra pazarolni az időnket. Általános jótanács, hogy a programokat bőségesen el kell látni megjegyzésekkel, mégpedig mindjárt megírásuk közben. A jól kommentezett programban mindenféle keresgélés, módosítás, javítás időigénye jelentősen kisebb, így az előzőleg befektetett munka végül bőségesen megtérül.

Elválasztok
Első mintaprogramunknak már majdnem minden eleméről volt szó. Nem foglalkoztunk még azzal, hogy mi választja el a különböző nyelvi elemeket, vagy mi jelzi az adott elem végét. Abban a sorban például, hogy

var n,faktorialis,szorzo:integer;

a var kulcsszót az n azonosítótól szóköz; az n, a faktorialis és a szorzo azonosítókat egymástól vessző, az integer azonosítótól pedig a kettőspont választja el.

Az ilyen elválasztó karakterek jelzik az egyes elemek határait, tehát például a nyelvi eszköz végét, de sokszor arra is utalnak, hogy mi következik.
Általában igaz, hogy minden nyelvi elemet olyan karakter zár le, tehát jelzi a végét, amely nem fordulhat elő az adott nyelvi elemben. Ha például nem lenne szóköz a var és az n között, akkor a gép a varn szót próbálná felismerni, és mivel nem tudná értelmezni, hibajelzést kapnánk.

Az n és a faktorialis szót vessző választja el egymástól, itt felesleges - bár megengedett - közéjük szóközöket is írni, hiszen a vessző önmagában is jól jelzi az őt megelőző azonosító végét.
Az elválasztó karaktereket nem választhatjuk meg tetszésünk szerint, a nyelv szabályai adják meg, hol, mikor, milyen karaktert használhatunk.
Ha más elválasztó karakter nincs két egymást követő nyelvi elem között, akkor a szóköz karaktert használjuk. Ilyen elválasztó, határoló hatásuk van még az írásjeleken és a műveleti jeleken kívül a megjegyzéseknek és a sor végének is.
Ahol valamilyen határoló karakter, például vessző vagy szóköz van, ott ez előtt és után is tetszőleges számú szóköz, megjegyzés vagy újsor jel lehet. Csak ez az utóbbi három elválasztó jel használható tetszőleges számban, más jel, például vessző vagy kettőspont nem.
A szóköz karaktert még arra is szoktuk használni, hogy tagoljuk, bekezdésekkel, kihagyásokkal jobban áttekinthetővé tegyük a programot. Erre ugyanaz érvényes, mint a kommentekre: nem kötelező alkalmazni, de érdemes.
Ugyanez a program megjegyzések és tagolás nélkül így nézne ki:

program Elso;var n,faktorialis,szorzo:integer;begin Read(n);faktorialis:=1;szorzo:=1;repeat faktorialis:=faktorialis*szorzo;szorzo:=szorzo+1;until szorzo>n;Write(n,' faktorialis = ',faktorialis) end.

Hogy tetszik?
A szimbólumelválasztókra vonatkozó szabályokat a 7.4. fejezetben ismertetjük.

Miután megismerkedtünk mintaprogramunk egyes elemeinek bizonyos általános tulajdonságaival, nézzük meg, hogyan működik maga a program!

1.2. Az első program jelentése

Kezdjük az ismerkedést a

var n,faktorialis,szorzo:integer;

sorral!

A var kulcsszó a variable szó rövidítése, ez magyarul változót jelent. A változó arra szolgál, hogy benne értéket tároljunk. Ha például kiszámítunk valamit egy képlettel, akkor ennek eredményét eltehetjük egy változóba. Később az így eltárolt értéket elővehetjük, hogy további számításokhoz felhasználjuk, vagy hogy a benne tárolt eredményt kiírassuk, például a gép képernyőjére. A változó lényegében egy memóriaterület használatát jelenti; az értéket a változóhoz hozzárendelt memóriaterületen tudjuk tárolni.
Hogy pontosan hol van ez a memóriaterület, mekkora, és milyen módon tároljuk benne az értéket, azt nem kell tudnunk: ez is az úgynevezett magas szintű programozási nyelvek egyik előnye. Elég a változó nevét, azaz azonosítóját megadni, ennek segítségével hivatkozni tudunk a változó értékére, akár megváltoztatni, akár használni akarjuk.
Nem véletlenül nevezzük változónak, hanem mert az egyszer eltett értéket meg is tudjuk változtatni. Ilyenkor a régi érték helyére az új kerül, a régi elvész, ettől kezdve csak az új értéket tudjuk használni, egészen addig, amíg ismét meg nem változtatjuk. Ezt úgy szoktuk mondani, hogy a változónak "aktuális értéke" van, ez az az érték, amit utoljára adtunk neki. Ha egy változónak még egyszer sem adtunk értéket, például azért, mert éppen most indult csak el a programunk, akkor a változó értéke "meghatározatlan", és értékének felhasználása hibát okoz.

A var sorában szerepel az integer szó is (kiejtése: intidzsör), ez magyarul azt jelenti, hogy egész, és egy úgynevezett adattípusnak a neve.
Változókat - és általában adatokat - ugyanis sokféle típussal deklarálhatunk a Pascal nyelvben, az adattípusok gazdag választéka ennek a programozási nyelvnek az egyik legnagyobb előnye. Különböző adattípusok használatával a különböző jellegű adatokat jellegüknek megfelelően tárolhatjuk és jellegüknek megfelelő műveleteket végezhetünk velük. Máshogyan kell tárolni és más műveleteket lehet végezni az egész számokkal, a tört értékek tárolására is alkalmas, úgynevezett valós számokkal, a karakter típusú adatokkal vagy a táblázatok tárolására képes tömbökkel.
A további fejezetekben rendre megismerkedünk a Pascal nyelv adattípusaival, az adott adattípuson végeztető műveletekkel és tipikus felhasználási módjaikkal.
Visszatérve az integer szóra: ez olyan típusnak a neve, amely egész értékek tárolására képes, ebben a feladatban ugyanis csupa egész számot használunk.

Ez a sor tehát változókat deklarál. A "deklaráció" magyarul körülbelül annyit jelent, hogy kijelentés. Ez a sor azt deklarálja, azaz azt jelenti ki, hogy a programban használni akarunk három változót, amelyeknek az n, faktorialis és szorzo neveket adjuk, a típusuk legyen egész, azaz egész számértékeket akarunk tárolni bennük. Ennek a sornak a hatására a program indulásakor a gép valahol a memóriában lefoglal a három egész típusú változó számára egy-egy megfelelő méretű területet. A változók értékét a deklaráció nem állítja be, az kezdetben meghatározatlan lesz.

Még néhány szó a sor alakjáról. A var kulcsszó azt jelenti, hogy ez után következik a változók deklarácója. Ez a változódeklarációs rész e programban egészen a begin kulcsszóig tart. Most csak három egész típusú változót deklaráltunk, hiszen ez is elég a feladat megoldásához. De deklarálhatnánk itt további, esetleg más, nem egész típusú változókat is.
A var szó után a változóneveket vesszővel elválasztva soroltuk fel. Az utolsó változó neve után kettőspont áll, utána pedig az integer szó, amely az adattípus neve. A deklarációt pontosvessző követi.
Egyelőre elegendő ennyit tudnunk a változók deklarációjának alakjáról.

Másodiknak nézzük ezt a sort:

faktorialis:=1;szorzo:=1; { kezdeti ertekek }

A sor végén álló megjegyzést már ismerjük. Előtte két, egymáshoz nagyon hasonló dolgot találunk, bennük a := speciális szimbólummal. A faktorialis:=1 egy utasítás, amelynek hatására a gép az 1 értéket teszi a faktorialis nevű változóba.
A := az értékadás jele. Az olyan utasítás, amelyben egy változó neve után ez a jel áll, úgynevezett értékadó utasítás. A := előtt megadott változónak adunk új értéket, a := után pedig azt kell megadni, hogy mennyi legyen ez az új érték. Ebben az utasításban az értéket egy szám adja meg. A faktorialis változóval a deklarálása óta nem csináltunk semmit, tehát értéke meghatározatlan volt, ez után már jól meghatározott értéket tárol. Az utasítás után egy pontosvessző áll, ez választja el a következő utasítástól:

szorzo:=1

amelyet ezek alapján már ismerünk: arra utasítja a gépet, hogy tegyen 1 értéket a szorzo változóba.
Utána szintén pontosvessző áll, ez elválasztja a következő utasítástól, amely a repeat kulcsszóval kezdődik. A megjegyzés, mint tudjuk, a program végrehajtása szempontjából olyan, mintha ott sem lenne.

A harmadik sor, amellyel megismerkedünk:

Read(n); { n beolvasasa }

A read szó magyarul: olvass vagy olvasson (kiejtése: ríd). Ez arra utasítja a gépet, hogy egy külső adatbeviteli eszközről olvassa be a következő értéket, és azt tegye el a zárójelek között megadott változóba, esetünkben n-be.
Hogy mi ez a külső adatbeviteli eszköz, az függhet például a géptől, amelyet használunk. Személyi számítógép esetén ez tipikusan a gép billentyűzete. Ha ezen most leütjük az 5-ös, majd az ENTER billentyűt (esetleg RETURN-t vagy NEW LINE-t, attól függően, hogy az adott billentyűzeten mivel kell egy sort befejezni), akkor a gép ezt a beírt értéket, az 5-öt teszi az n változóba.

A read utasítás tehát beolvas egy értéket, és azt a zárójelek között megadott változóba teszi. Ez hasonlít az értékadó utasításra, hiszen az is változó értékének beállítására való, sok köztük azonban a különbség is. Például nem a program szövege határozza meg, hogy mit tesz a változóba, ezt kívülről, esetünkben a gép billentyűzetéről kapja. Maga a read szó is azonosító - tehát nem kulcsszó mégpedig eljárás (angolul: procedure, ejtsd: proszidzsör) azonosítója. Az eljárás utasítássorozatot jelent, az eljárás használata pedig ennek az utasítássorozatnak a végrehajtását. A read eljárást megvalósító utasításokat a gép a program végrehajtásra való előkészítése során automatikusan a programunkhoz fűzi, és ezek a program végrehajtásakor elvégzik számunkra a kívánt adatbeolvasási műveletet.

A gép a programok utasításait olyan sorrendben hajtja végre, ahogyan ezeket a program szövegében leírtuk, hacsak olyan utasítást nem adunk, hogy térjen el ettől a sorrendtől. Az eddig megvizsgált három utasítást a leírás sorrendjében fogja végrehajtani. A következő táblázatban a három utasítást és a változók értékeit látjuk az egyes utasítások végrehajtása előtt, illetve után. A kérdőjel egy változó értékénél azt jelzi, hogy az meghatározatlan.
Feltesszük, hogy 5 volt a bemeneti adat.

utasítás
változók értéke
n
faktorialis
szorzo
read(n)
?
?
?
faktorialis:=1
5
?
?
szorzo:=1
5
1
?
 
5
1
1

Most ismét ugorjunk egy kicsit előre, folytassuk a program vizsgálatát a

szorzo:=szorzo+1

utasítással! Ez is értékadó utasítás, csakhogy itt a := után egy kifejezés található. A gép ilyenkor kiszámítja a kifejezés értékét, és amikor kész, az eredményt a bal oldali változóba teszi. Ha például a szorzo változó értéke 1 volt, akkor a kifejezés értéke 2 lesz; ha 18 volt, akkor 19; ha - 4 volt, akkor - 3. Nagyon fontos a sorrend: először történik a kifejezés értékének kiszámítása, ehhez a szorzo változó régi értékét veszi figyelembe, s csak miután ez kész, akkor tárolódik az eredmény, akkor állítódik be szorzo új értéke.
Teljesen hasonlóan működik a

faktorialis:=faktorialis*szorzo

utasítás is. A * a szorzás műveleti jele. Ha tehát az utasítás előtt a faktorialis értéke 1 és a szorzo értéke 2 volt, akkor utána a faktorialis értéke 2 lesz. Ha előtte a faktorialis értéke 24 és a szorzo értéke 5 volt, akkor utána a faktorialis értéke 120 lesz.

Most egy olyan utasítás következik, amely arra szolgál, hogy vezérelje a program végrehajtásának menetét, azaz a segítségével azt írjuk elő, hogy a gép ne egyszerűen a leírás sorrendjében hajtsa végre az utasításokat. Ez a repeat utasítás. A mi programunkban így fordul elő:

repeat { ciklus }
  faktorialis:=faktorialis*szorzo;
  szorzo:=szorzo+1; { kovetkezo reszsorozat }
until szorzo>n;

A repeat utasítás a repeat és until kulcsszavakból, közöttük további utasításokból és az until után a szorzo>n kifejezésből áll. Maga a repeat szó annyit jelent, hogy ismételd, az until pedig azt, hogy mígnem. (Kiejtésük: ripít, illetve antil.) Az until szó után álló szorzo>n a szorzo és az n változó értékének az összehasonlítását jelenti; a gép megvizsgálja, hogy a szorzo értéke nagyobb-e, mint n. Az összehasonlítás eredménye nem szám, hanem az "igaz" vagy "hamis" érték: ha a szorzo értéke nagyobb volt, akkor az összehasonlítás eredménye igaz, ha nem nagyobb, tehát kisebb vagy egyenlő, akkor hamis.
Ennek az összehasonlítási műveletnek az az érdekessége, hogy bár egész értékek szerepelnek benne, a művelet eredménye mégsem egész típusú érték, hanem logikai. Így nevezzük ugyanis egy olyan mennyiség típusát, amely igaz vagy hamis értéket vehet fel.

A repeat utasítás végrehajtása úgy történik, hogy a gép először végrehajtja a repeat és az until kulcsszavak között felsorolt utasításokat (amelyeket egymástól pontosvesszővel választunk el), mégpedig a megadásuk sorrendjében, majd megvizsgálja az until után álló feltételt. Ha ez nem teljesül, tehát hamis, akkor ismét végrehajtja a repeat és until közötti utasításokat, megvizsgálja a feltételt, és így tovább. Ha a feltétel akár az első, akár valamelyik megismételt végrehajtáskor igaz, akkor a gép befejezi a teljes repeat utasítás végrehajtását, és a program az ezt követő utasítással folytatódik.
Tömören: a repeat utasítás a benne felsorolt utasítások ismételt végrehajtását jelenti mindaddig, amíg az until utáni feltétel igazzá nem válik:

repeat = ismételd
  ...
until feltétel= mígnem feltétel

Most nézzük meg, hogyan történik programunkban a repeat utasítás végrehajtása! Ezt úgy tesszük meg, hogy leírjuk a végrehajtás sorrendjében az utasításokat, illetve a feltételt, és melléje az éppen beállított változó új értékét, illetve az egyenlőségvizsgálat eredményét.

 
n
szorzo
faktorialis
feltétel
repeat indulásakor
5
1
1
 
faktorialis:=faktorialis*szorzo    
1
 
szorzo:=szorzo+1  
2
   
szorzo>n      
hamis
faktorialis:=faktorialis*szorzo    
2
 
szorzo:=szorzo+1  
3
   
szorzo>n      
hamis
faktorialis:=faktorialis*szorzo    
6
 
szorzo:=szorzo+1  
4
   
szorzo>n      
hamis
faktorialis:=faktorialis*szorzo    
24
 
szorzo:=szorzo+1  
5
   
szorzo>n      
hamis
faktorialis:=faktorialis*szorzo    
120
 
szorzo:=szorzo+1  
6
   
szorzo>n      
igaz

A repeat utasítás tehát egy programrész többszöri, azaz ciklikus végrehajtását teszi lehetővé, ez a Pascal nyelv egyik úgynevezett ciklusutasítása.
Az ismételt programrészt a ciklus magjának, vagy röviden ciklusmagnak nevezzük, a ciklus folytatásának vagy befejezésének feltételét pedig ciklusfeltételnek. A repeat ciklusnál az until szó után a ciklus befejezésének feltételét kell megadni, a ciklus ugyanis akkor fejeződik be, ha ez a feltétel teljesül. Ezt kilépési feltételnek is szokás nevezni.
Ha a ciklus befejeződött, akkor a program végrehajtása a ciklust követő utasítással folytatódik.

A repeat ciklus befejezése után egyetlen utasításunk maradt még:

Write(n,' faktorialis = ',faktorialis)

A write szó azt jelenti, hogy írd (kiejtése: rájt), és a read-hez hasonlóan egy eljárás neve. Kiírja a zárójelben vesszővel elválasztva felsorolt dolgokat a képernyőre, vagy nyomtató segítségével papírra, attól függően, hogy gépünk az eredményeket hová írja.

A zárójelben vesszővel elválasztott lista első eleme az n változó, ennek az értékét írja ki a write utasítás, esetünkben 5-öt.
Avessző után ' faktoriális = ' áll, tehát aposztrófok (felsővesszők) között egy karaktersorozat, egy szöveg. Az aposztrófok között megadott karaktersorozatot karakterláncnak, idegen szóval stringnek (kiejtése: sztring) nevezzük. A magyar nyelvű szakirodalomban használják még e fogalomra az írásjelfüzér vagy egyszerűen jelfüzér szót is.
A stringet a write utasítás változtatás nélkül, karakterről karakterre a megadott alakban írja ki, de a kezdő és záró aposztrófok nélkül, hiszen azok csak a karakterlánc elejének és végének jelzésére szolgálnak.
Ezután a zárójelben megadott listán ismét vessző, majd a faktorialis változó neve következik, ez a változó értékének megjelenítését okozza.
A teljes kiírt szöveg tehát a következő:

5 faktoriális = 120

Ez programunk futásának eredménye.

Végül a program szövegét lezáró end és a pont következik, innen tudja a gép, hogy befejeződött a program.

Ha módunkban áll, futtassuk le a programot, először 5-öt, azután különböző értékeket használva bemeneti adatként.
Mit tapasztalunk? Próbáljunk meg magyarázatot találni a jelenségekre: hajtsuk végre a programot kézzel, azaz papír-ceruza módszerrel, ahogyan azt láttuk!

1.3. Szintaxis és szemantika: forma és tartalom

Eddigi ismerkedésünk során már láthattuk a nyelv néhány szabályát.
Ilyen szabály volt például, hogy az értékadó utasítás egy := speciális szimbólumot tartalmaz, amelynek bal oldalán változó, jobb oldalán kifejezés áll. Egy másik szabály, hogy egy értékadó utasítás végrehajtásakor a gép először a := után álló kifejezés értékét határozza meg, és ez után tárolja az eredmény értékét a bal oldali változóban. Az első a program írásának formai, azaz alaki szabálya, az utóbbi pedig a program végrehajtásáé.
A program írásának formai szabályait "szintaktikai" szabályoknak, a program jelentését, működését megadó szabályokat "szemantikai" szabályoknak nevezzük.
Ha eltévesztünk valamit, vagy nem tartunk be valamilyen szabályt, az hibát okoz. Ahogyan vannak szintaktikai és szemantikai szabályok, úgy a hibák is lehetnek szintaktikai, illetve szemantikai hibák.

Szintaktikai hiba az, ami a formai követelmények be nem tartásából származik. Például ha szóközt írnánk a : és = közé az értékadó utasításban, vagy a begin kulcsszó helyett azt írnánk, hogy megin (és ez már nemcsak a Pascal, de a magyar nyelv szintaktikájának sem felel meg).

A szemantikai hiba ezzel szemben az, hogy a program jelentése nem megfelelő, azaz a program nem, vagy nem mindig ad helyes eredményt. Mintaprogramunkban például, ha a faktorialis változó értékét először nem 1-re, hanem 0-ra állítanánk, akkor bármi legyen is az n bemeneti adat értéke, az eredmény mindig 0 lenne, hiszen a repeat ciklusban minden lépésben nullát szoroznánk a szorzo-val. Ez nem formai, azaz szintaktikai hiba, hanem szemantikai, hiszen a

faktorialis:=0

utasítás formailag ugyanúgy jó, mint a

faktorialis:=1

Szemantikai hiba lenne az is, ha a szorzo vagy a faktorialis változó nem kapna kezdeti értéket, vagy fordított lenne a két utasítás sorrendje a repeat utasításban.

Jó lenne tudni, hogyan lehet a szintaktikai és szemantikai hibákat elkerülni, vagy ha mégis hibás programot írunk, felfedezni és kijavítani.
A hibákat elvileg úgy lehet elkerülni, hogy jól ismerjük az adott nyelvet és eleve jó programot írunk. Ez egyszerűen hangzik, megvalósítani azonban nagyon nehéz. Bizonyos tervezési módszerek, programozási elvek sokat segítenek abban, hogy már a megíráskor jó legyen a programunk, vagy legalábbis kevés és viszonylag könnyen kijavítható hibát kövessünk csak el. A könyv további részében rendszeresen kitérünk arra is, hogy az adott nyelvi eszköz használatakor melyek a tipikus hibák, és mi ezek elkerülésének módja.

Most foglalkozzunk azzal, hogyan lehet a hibákat felfedezni és kijavítani!
Bár ebben a könyvben a Pascal nyelvű programozással foglalkozunk, és nem azzal, hogy hogyan is lesz egy ilyen programból a számítógép által közvetlenül végrehajtható, ún. gépi utasítássorozat, a hibafelderí-tés módszereinek megértéséhez szükségünk lesz a fordítóprogram fogalmának megismerésére.
A szintaktikai hibák felfedezése és kijavítása elég könnyű, ezeket a hibákat ugyanis maga a számítógép, illetve annak egy programja megkeresi és kijelzi. Ez a program a fordítóprogram (angolul: compiler, kiejté-se: kompájler). Az a feladata, hogy a Pascal nyelvű programot a gép számára közvetlenül érthető és végrehajtható formába alakítsa át, azaz a programot lefordítsa a gép nyelvére. A számítógépek ugyanis nem tudják közvetlenül végrehajtani a Pascal nyelvű programokat, mert azok túl bonyolultak a számukra. A számítógépek a Pascal utasításoknál lényegesen egyszerűbb utasításokat képesek csak végrehajtani.

A Pascal fordítóprogram számára a Pascal nyelvű program a bemeneti adat, a gépi nyelvű program pedig a fő kimeneti adat. De ha a fordító hibát talál programunkban, akkor ezt is kiírja, tehát, ha keletkeznek hibaüzenetek, a fordítóprogramnak ezek is kimeneti adatai. A hibaüzenetek egy személyi számítógép esetén a gép képernyőjén jelennek meg. Általában elég részletesek és érthetőek ahhoz, hogy a szintaktikai hibákat segítségükkel megtaláljuk, és a program szövegében kijavítsuk.

Hogy a program lefordítása hogyan történik, és milyen az így létrehozott gépi nyelvű program, az a Pascal nyelvben programozó felhasználó számára rendszerint közömbös. A pontosság kedvéért azonban meg kell jegyezni, hogy sokszor nem elég a programot lefordítani, hanem ahhoz, hogy a gépen tényleg lefuttatható legyen, további műveletek is szükségesek. Ilyen további művelet lehet például, hogy a lefordított programhoz hozzá kell fűzni további programrészeket, például azokat, amelyek a read és a write eljárásnak megfelelően a program adatainak beolvasását és kiírását végzik. Azt a programot, ami ezt elvégzi általában "szerkesztőprogramnak" nevezik (angolul: linkage-editor, kiejtése: linkidzs-editor).
Ettől a "munkafolyamattól" mind a Turbo Pascal, mind a HiSoft-Pascal használata esetén mentesülünk, mert a keretrendszer ezt automatikusan elvégzi fordításkor.

A fordítóprogramnak a szintaktikai hibák felismerését a nyelv szintaktikai szabályai, kötöttségei teszik lehetővé.

A szemantikai, tehát működésbeli hibák kiderítése és kijavítása általában lényegesen nehezebb. Ezeket a fordítóprogramok általában nem képesek felfedezni, tehát megtalálásuk a programozó feladata. Például úgy találhatjuk meg őket, hogy a programot több különböző, gondosan megválasztott bemeneti adattal is lefuttatjuk, és figyeljük a futási eredményeket. Ha ezek nem felelnek meg annak, amit elvileg kapnunk kellene, akkor a programban valamilyen tartalmi hiba van, és a kapott hibás eredményből legtöbbször a hibái helyére és a javítás mikéntjére is következtetni tudunk. Ha ez nem sikerülne, akkor tehetünk például további kiírási utasításokat is a programba: ne csak a rossz végeredményt lássuk, hanem azt is, hogy hogyan, milyen lépések és részeredmények vezettek erre. Ha jól választjuk meg az e célból beiktatott kiíró utasítások helyét és a kiírandó változókat, akkor a kapott adatokból jobban láthatjuk a program belső működéséti tehát a hiba helyét is könnyebben felfedezhetjük. A szemantikai hibák megkereséséhez bizonyos gyakorlat kell, módszereivel a nyelv jobb megismerése után érdemes foglalkozni.

A Pascal nyelv egyik erőssége, hogy bizonyos szemantikai jellegű hibákat szintaktikai hibaként képes kezelni, vagy pontosabban fogalmazva: lehetővé teszi olyan program írását, amely bizonyos szemantikai hibákat automatikusan jelez. Ez adja a Pascal nyelvű programok jobb megbízhatóságát olyan nyelvekkel szemben, mint például a BASIC, a FORTRAN vagy a C.

Növeli a megbízhatóságot például a következő szabály:


A Pascal programban használt minden azonosítót az első felhasználás előtt deklarálni, illetve definiálni kell. Egyetlen kivétel van (Illetve HiSoft-Pascalban ez a kivétel sem létezik): a mutató típus definíciójában használhatunk még nem definiált típusazonosítót is, de azt még ugyanebben a típusdefiníciós részben meg kell adni.
Az előre definiált azonosítókat úgy kell tekinteni, mintha definiálásuk a program szövege előtti részben történt volna meg.


A mintaprogramban az n, a faktorialis és a szorzo változókat deklaráltuk, az integer, read és write azonosítók pedig előre deklarált azonosítók voltak, azaz olyanok, amiket nem a programozónak kell deklarálnia, a fordítóprogram ismertnek tekinti őket.

Az előbbi szabály növeli a Pascal programok megbízhatóságát. Ha eltévesztjük például a faktorialis szót, és valahol véletlenül faktoralis-t írunk, akkor ez szintaktikai hibát fog okozni, mégpedig olyasféle hibaüzenettel, hogy:

*** Definiálatlan azonosító ***
Turbo Pascalban: Unknown identifier or syntax error

Azoknál a programozási nyelveknél, ahol nem kell minden változót deklarálni, olyan nevet is használhatunk valahol a programban, ami eddig nem fordult elő. Ilyenkor ezt a fordító automatikusan új változónak tekinti. Az automatikus deklarációt tartalmazó nyelveknél tehát egy változó nevének véletlen elírása nem okoz szintaktikai hibát, az elírás csak a program futása során derül ki, megtalálása is nehezebb.

Fejezzük be első programunk megismerését a program tesztelésével: nézzük meg, mindig helyes eredményt ad-e. Adjunk meg különböző bemeneti adatokat, és nézzük meg, mi lesz az eredmény! A következő eredmények olyan gépen születtek, mely az egész számokat 16 biten ábrázolja, így maxint értéke 32767.

bemeneti adat
eredmény
Értékelés
5
120
helyes
6
720
helyes
7
5040
helyes
8
-25216
HIBÁS!
9
-30336
HIBÁS!
10
24320
HIBÁS!
11
5376
HIBÁS!
12
-1024
HIBÁS!
1
1
helyes
0
1
helyes
-1
1
helyes? hibás?

Úgy tűnik, a program csak akkor működik jól, ha a bemeneti adat 0 és 7 között van. HiSoft-Pascalban 7-nél nagyobb értéket megadva a program futása hibaüzenettel meg is szakad.
8 és 9 esetén negatív eredményt kapunk, 10 és 11 esetén pozitív ugyan az eredmény, de feltűnően alacsony, 0 alatt pedig mindig 1. 7 fölött a hiba onnan származik, hogy az eredmény nagyobb lenne, mint maxint, azaz az ábrázolható legnagyobb egész érték. Ha ugyanezt a programot lefuttatjuk egy olyan gépen, amely az egészeket 32 biten tárolja, és így maxint értéke 2147483647, akkor egészen n=12-ig helyes eredményt kapunk, de 13!=6227020800 lenne, ami annál a gépnél sem ábrázolható.

Az, ha egy szám olyan nagy, hogy nem ábrázolható az adott gépen, nem jelenti azt, hogy semmilyen módszerrel sem lenne tárolható. Ha egy gép az egész adatok tárolására 16 bitet használ, tehát maxint=32767, azért még olyan nagy számot is tárolhatunk benne, minta 6227020800. Például 5 egész változót használunk, mindegyikben a számból csak két decimális számjegynek megfelelő értéket, azaz az elsőben a 62, a másodikban a 27 stb., az ötödikben a 00 számjegypár értékét tárolnánk. Az ilyen adatcsoporttal viszont a számítógép nem tud egyetlen lépésben műveletet végezni, nem tud két ilyen számot összeadni vagy összeszorozni. A gép utasításai között ugyanis csak 16 bites műveletek szerepelnek, amelyek tehát csak maxint-nél nem nagyobb számok esetén használhatók.
Amikor a továbbiakban arról beszélünk, hogy egy szám nem ábrázolható az adott gépen, ez azt jelenti, hogy az eredeti, a gép által közvetlenül használható módon nem ábrázolható, tehát nem vesszük figyelembe az előbb említetthez hasonló trükköket.

Ha egy számításban olyan eredmény, vagy akár csak részeredmény adódik, amely már nem fér el a gép által használt számábrázolással, akkor "túlcsordulás" történik. Ez azt jelenti, hogy a keletkezett értéknek csak egy részét képes tárolni a gép, a többi elvész, ez nyilvánvalóan hibához vezet.
Az egész értékek túlcsordulását egyes megvalósítások jelzik, mások nem. A Turbo Pascal például nem jelzi, a Pascal 8000 vagy a HiSoft Pascal viszont igen. Ezért érdemes alaposan áttanulmányozni az általunk használt változat kézikönyvét.

A felhasználó számára az lenne jó megoldás, ha a program 0-nál kisebb bemeneti adat esetén nem 1-et írna ki, hanem például ilyen üzenetet:

*** Negatív szám faktoriálisa nem értelmezett ***

Hasonlóan, 7-nél nagyobb bemeneti adat esetén is jó lenne, ha nem a kiírt eredmény előjeléből vagy nagyságából kellene a hibára következtetni, hanem egyértelmű üzenetet kapnánk:

*** Az eredmény túl nagy lenne, nem tudom kiszámolni ***

Ahhoz, hogy ennek megfelelően át tudjuk alakítani a programot, további nyelvi elemeket kell megismerŹnünk. Előbb azonban a nyelv szabályainak megadására az eddigieknél pontosabb és tömörebb leírási módszert kell választanunk. Erre szolgál a szintaktikai diagram, a folyamatábra és a szabályösszefoglaló, A következő alfejezetek ezek használatát ismertetik.

1.4. Szintaxisábra

A szintaktikai, azaz formai szabályok egy részének leírására "szintaktikai diagramokat", szintaxisábrákat fogunk használni. A szintaxisábra megadja, hogy az adott nyelvi eszköz milyen egyéb nyelvi eszközökből áll és mi ezek lehetséges sorrendje.
Például az azonosító formai szabályait megadó szintaxisábra:

A szintaktikai diagramon a nyilak mentén kell haladni. A fenti esetben azt kapjuk, hogy egy azonosító legalább egy betűből kell álljon, utána vagy az ábra végére jutunk, vagy elágazunk lefelé. Ekkor az azonosító további betűt vagy számjegyet tartalmazhat, ez után újra befejeződhet, vagy ismét elágazhat lefelé stb. Ez pontosan megfelel az azonosító eddig megismert írásmódjának: betűvel kezdődik és betűkkel vagy számjegyekkel folytatható, tetszőlegesen hosszú lehet, de legalább egy betűből áll.
A szögletes keret - amit itt a betű és a számjegy szavak körül látunk - azt jelzi, hogy ezek olyan nyelvi elemek, amelyeknek az alakját további szintaxisábrák adják meg. Ezek tehát közbeeső, nem végső, "nemterminális" szimbólumok, hiszen további magyarázatra szorulnak.
Ennek megfelelően most adjuk meg a számjegy szintaktikai diagramját!

Itt 10 irányban ágazhatunk el, mindegyik ágban a tízes számrendszer egy-egy számjegye áll.
Ezek a számjegyek körökben vannak. Az ívelt keret vagy a kör egy-egy nyelvi elem körül a szintaktikai diagramban azt jelenti, hogy ezek a nyelvi elemek már nem igényelnek további magyarázatot. A programban pontosan úgy kell őket megadni, ahogyan az ívelt keretben szerepelnek. Ezeket végső, azaz "terminális" szimbólumoknak nevezzük.
A szintaxisábrákkal nem lehet megadni a nyelv összes szintaktikai szabályát. Nem szintaktikai diagrammal, hanem szövegesen adjuk meg például azt a szabályt, hogy az előre definiált szimbólumok kivételével minden azonosítót definiálni, illetve deklarálni kell, vagy azt, hogy nem használhatunk a kulcsszavakkal megegyező azonosítót.

Vannak olyan terminális nyelvi elemek, amelyeket szintén inkább szöveges formában, vagy egyszerű felsorolással adunk meg. Ilyen például a betű fogalma, erre már láttuk a szabályt az 1.1. alfejezetben. Ebben a szintaktikai diagramban a 226 betű miatt 52 irányba kellene elágazni, ez nem lenne szemléletes megodás. Talán még a számjegy fogalmát is kényelmesebb egyszerű felsorolással megadni.

Végül vannak olyan nemterminális nyelvi elemek, amelyeket semmilyen formában sem adunk meg, mert jelentésük nyilvánvaló. Ha például egy szintaktikai diagramban az szerepel, hogy "típusazonosító", akkor világos, hogy itt egy olyan azonosítónak kell állnia, amely valamilyen típust, azaz adattípust ad meg.

1.5. Folyamatábra

Folyamatábra segítségével a program végrehajtásának menetét, az egyes végrehajtandó tevékenységeket, feltételvizsgálatokat, elágazásokat, illetve az egyes programrészek közötti vezérlésátadást, tehát a program algoritmusát szokás megadni.
Folyamatábrát nemcsak az egyes nyelvi elemek, hanem kész programok vagy programrészek működésének leírására, dokumentálására is használunk. A jó folyamatábra egészében és részleteiben is jobban áttekinthető, mint egy hosszadalmas szöveges leírás. Első programunk eredeti változatának folyamatábrája jobb oldalon látható.

Ez némileg hasonlít a szintaktikai diagramra, mégis nagyon lényeges különbség van közöttük: a folyamat ábra a program vagy programrészlet működését, nem pedig leírásának alakját adja meg.

A folyamatábrán is van jelentősége annak, hogy milyen kereteket használunk.
A folyamatábra elemei:

  • Program vagy programrész kezdete:

Csak kijárata van.
Beírt szöveg: a program vagy programrész (eljárás, függvény) neve, és, ha vannak, formális paraméterei.

  • Program vagy programrész vége.
    Mint a program kezdete, de ennek csak bejárata van.
    Beírt szöveg: Vége vagy Stop, eljárásnál és függvénynél lehet még Visszatérés is.

  • Tevékenység, azaz olyan programrész, amely összetartozó, egymás után elvégzendő műveleteket tartalmaz:

Egy be- és egy kijárata van.
Beírt szöveg: a művelet vagy műveletek rövid szöveges leírása.
Nem Pascal utasításokat írunk ide, ahhoz elolvashatjuk magát a programot. Célszerű, ha a program megjegyzései és a folyamatábra elemei nagyon hasonló szövegűek, így könnyű a megfeleltetés a folyamatábra és a program között.

Egy be- és két kijárata van, a bejárat felül, a kijáratok pedig oldalt, vagy oldalt és alul lehetnek. A kijáratokhoz mindig oda kell írni, hogy a feltétel melyik értékéhez tartoznak.
Beírt szöveg: a megvizsgálandó feltétel szöveges leírása. Ne a programban szereplő, a feltételt megadó logikai kifejezést írjuk ide, hanem az eldöntendő kérdés lényegét szöveges formában. Ennél a folyamatábra-elemnél is jó, ha a használt szöveg a program szövegében is megjelenik a megfelelő feltételvizsgálatnál, megjegyzés formájában.

Egy be- és egy kimenete van.
A második változat arra az időre emlékeztet, amikor a beolvasás elsősorban lyukkártyáról történt.
Beírt szöveg: minek a beolvasása történik, esetleg az erre használt változók neve.

Egy be- és egy kimenete van.
A második alak arra emlékeztet, hogy az eredményeket papírra nyomtatjuk ki.
Beírt szöveg: a kiírt eredmények leírása.

Egy be- és egy kijárata van.
Beírt szöveg: a hívott szegmens neve, esetleg "hívás", a keret alakja ezt önmaga is jelzi.

Az irányt nyíl jelzi, a lefelé vagy jobbra tartó nyíl elhagyható.
A kereszteződés nem jelent csatlakozást.

Becsatlakozni csak összeköttetésbe lehet, tevékenységbe, elágazásba stb. nem.

A beírt szöveg a folytatás helyének ábraszáma és az ábrán belüli csatlakozási pont sorszáma. Tanácsos elkerülni a többoldalas folyamatábrákat, jobb a programot olyan kis egységekben, pl. eljárásokban, függvényekben megírni, hogy ezek folyamatábrája elférjen egy-egy oldalon.

Akkor használjuk, ha feltétlenül külön akarjuk választani a tevékenység tömör leírását egy bővebb magyarázattól.

1.6. Szabályok leírási módja

A Pascal nyelv szabályait az erre vonatkozó ISO szabvány szerint mutatjuk be. Ez nem egyszerűen a szabvány ismertetését jelenti, hiszen a gyakorló programozó számára egy szabvány meglehetősen tömör szövege nem a legjobb segítség. Ezért a nyelv szabályait a szabványnál bővebben, magyarázatokkal, mintapéldákkal és megjegyzésekkel kiegészítve tárgyaljuk. Egy-egy fogalom bemutatásakor utalunk a tulajdonságok legfontosabb következményeire és az adott nyelvi elemhez szorosan kapcsolódó további nyelvi elemekkel való viszonyára is.
Ez lehetővé teszi, hogy a szabályok leírását ne csak a nyelv tanulásához, hanem kézikönyvként is lehessen használni.

A szabályokat nyomdatechnikailag is el fogjuk különíteni: az adott nyelvi elemhez tartozó szabálycsoport elejét és végét egy-egy vízszintes vonal jelzi, a szabálycsoport szövege pedig a szintaktikai diagramok és folyamatábrák kivételével dőlt betűs. A szabálycsoportok végén legtöbbször magyarázatok és megjegyzések is vannak. Ezeket zárójelbe tett betűkkel jelöltük meg, pl. (a), (c), a szabálycsoportban pedig jelezzük, melyik megjegyzés tartozik az adott szabályhoz.

A Pascal nyelv minden szabályát ilyen szabálycsoportokba gyűjtve adjuk meg. Mivel ezek az összefoglalók elég részletesek lesznek, a szabályok megadása után azok további magyarázatára már nem lesz szükség, legfeljebb további mintapéldákat mutatunk be a nyelvi elem helyes, illetve - ami legalább ennyire tanulságos - hibás alkalmazására.
A szabálycsoportok a nyelvi elemekre vonatkozó minden ismeretet megadnak. Ennek a következménye, hogy előfordulnak bennük olyan fogalmak is, amelyeket az addigi ismeretek alapján nem érthetünk meg teljesen. Ezeket nyugodtan ugorjuk át, amikor a hiányzó fogalmakat részletesen megtárgyaljuk, ismét megadjuk a többi nyelvi elemmel való lényegesebb kapcsolatukat is.
Ilyen formában adtuk meg a kulcsszó fogalmát, és a következő pont is így adja meg a repeat utasításra vonatkozó szabályokat.

Az első fejezet eddigi részében megismerkedtünk a Pascal programozási nyelv egyes elemeivel és azokkal az eszközökkel, amelyeket ebben a könyvben a nyelv leírására használni fogunk.

"Úgy tűnt neki, mintha már ezerszer ismétlődött volna a történet...
Az örök visszatérés volt a vég nélküli vég!"

[0] 194. oldal

1.7. A repeat utasítás

A repeat utasítás egyike a Pascal nyelv három ciklusutasításának. A továbbiakban a repeat utasítás vagy repeat ciklus a teljes ciklusutasítást jelenti, nem tévesztendő össze a repeat kulcsszóval, amelyről ez az utasítás a nevét kapta.
Szabályai:


A repeat utasítás:

1. Alakja:

2. Az utasítás végrehajtása: a gép végrehajtja a repeat és until kulcsszavak között megadott utasítást vagy utasításokat - ezt ciklusmagnak is szoktuk nevezni majd kiértékeli az until után álló logikai kifejezést.
Ha ez igaz értékű, a repeat utasítás végrehajtása befejeződik, azaz a program a teljes repeat utasítást követő utasítással folytatódik.
Ha a logikai kifejezés értéke hamis, a gép ismét végrehajtja a repeat és az until közötti utasítást, vagy utasításokat, majd ismét megvizsgálja a logikai kifejezést stb. (a)(b)
A ciklusmag végrehajtása legalább egyszer biztosan megtörténik.

3. A repeat és az until szó között pontosvesszővel elválasztva tetszőleges számú utasítás megadható, de lehet a ciklusmag üres is. (c)

Magyarázatok, megjegyzések:

(a) Mivel a repeat ciklus akkor fejeződik be, amikor az until utáni logikai kifejezés igazzá válik, ezt a logikai feltételt a repeat ciklus leállási feltételének nevezzük.

(b) Ahhoz, hogy a ciklus valamikor befejeződjön, a feltételben olyan változót vagy függvényt kell használni, amelynek értéke a ciklus végrehajtása során megváltozik.

(c) A while és a for utasításnál valamivel kényelmesebbé teszi a repeat utasítás használatát, hogy a repeat és until szavak határolják a ciklusmagot képező utasításokat Az előbbi kettőnél ugyanis a ciklusmag, azaz az ismétlendő műveletrész csak egyetlen utasításból áll, ha mégis több utasítást akarunk ismételni, kénytelenek vagyunk utasításcsoportot (begin...end) használni.
Meglehetősen ritkán fordul elő az az eset, hogy a repeat ciklusmagja üres, tehát a repeat és until kulcsszavak között egyetlen utasítás sincs. Ilyenkor a leállási feltételben legalább egy függvényt kell használni, hiszen ha a logikai kifejezésben csak változók és konstansok szerepelnek, ezek értéke sohasem változik meg, a ciklus vagy egyszer sem, vagy vég nélkül ismétlődik.
A Turbo Pascalban létezik egy KeyPressed nevű logikai típusú függvény, amely akkor válik igazzá, ha a gép billentyűzetén leütnek egy tetszőleges billentyűt. Ha a gép futását ideiglenesen meg akarjuk állítani, például azért, hogy a további futás során keletkező üzenetek, eredmények ne írják felül a képernyő pillanatnyi tartalmát, amíg a felhasználó a képernyőre írt eredményeket tanulmányozza, a következő, üres ciklusmaggal rendelkező repeat utasítást szoktuk használni:

repeat until keypressed

Itt tehát a ciklus leállásának, azaz a program folytatásának az a feltétele, hogy leüssenek egy billentyűt. Magában a ciklusban a várakozáson kívül semmi érdemlegeset nem kell tennünk.


1.8. Az egész (integer) típus

Az egész típusra vonatkozó szabályok:


Az egész (integer) típus:

1. Az egész adattípus a matematikában megszokott pozitív és negatív egész számok egy véges tartományát és a nullát jelenti. Előre definiált, azaz szabványos típus, előre definiált típusazonosítója az integer, értékkészlete a -maxint és a maxint közötti egész számok halmaza, ahol maxint előre definiált egész konstans, értéke az adott megvalósításban definiált. (a)

2. Az egész számok írásmódjának formai szabályai: Az egész típusú szám decimális számjegyek sorozata, amelyet előjel előzhet meg. A pozitív előjel elhagyható.

3. Az egész sorszámozott típus, ord értéke önmaga: ord(k)=k

4. Egész típusú adattal a következő műveletek végezhetők:

Kifejezésekben egészekkel az alábbi műveletek végezhetők:

Ha valamelyik művelet eredménye nincs a megengedett -maxint...maxint tartományban, az hibát okoz. (b)
Az operandusok egészek.

5. Egészek előre deklarált függvényei; a k-val jelzett paraméter integer típusú kifejezés lehet.

Magyarázatok, megjegyzések:

(a) maxint értéke attól függ, hogy az adott gép az egészeket hány biten ábrázolja. Mivel gyakorlatilag minden számítógép úgynevezett kettes komplemens (kettes számrendszeren alapuló) kódot használ, n bites ábrázolás esetén

maxint = 2^(n-1) - 1

A gépek egy része (pl. IBM PC, C64, ZX-Spectrum, Enterprise, PDP-11, TPA-11) az egészeket 16 biten tárolja, ezeknél maxint=32767. Más gépek (pl. IBM 360, 370, a legtöbb munkaállomás) 32 bitet használnak az egészek tárolására, itt maxint=2147483647.
Az integer típus úgy is tekinthető, mintha definíciója a következő intervallumtípus lenne:

integer = -maxint..maxint

A kettes komplemens számábrázolás következtében a gépek a valóságban a -maxint-1 értékét is tárolni tudják, de ezt a tulajdonságot a szabványosság megtartása érdekében nem szoktuk kihasználni.
Ez az érték onnan adódik, hogy egy bitnek két különböző értéke lehet, 0 vagy 1, két bitnek 2*2=2^2, 3 bitnek 2*2*2=2^3, n bitnek tehát 2^n. Ezt pozitív és negatív számok között kell felosztanunk úgy, hogy a fele pozitív, a fele negatív számot jelentsen. Mivel azonban a nulla nem pozitív, nem negatív, de első bitje a pozitív számokhoz hasonlóan 0, ez a lehetséges pozitív számok számát eggyel csökkenti.

Egész típusú adatok bithosszát a következő programmal határozhatjuk meg például:

program egeszek_merete;
var ertek,meret:integer;
begin
  ertek:=maxint;meret:=1;
  repeat
    ertek:=ertek div 2;
    meret:=meret+1
  until ertek=0;
  writeln('az egesz szamok merete = ',meret,' bit.')
end.

A program nevében használtuk az alulvonás karaktert: _, amelyet a legtöbb fordítóprogram, és így az e könyv mintapéldáinál használt Turbo Pascal is betűnek tekint (de a HiSoft Pascal nem fogadja el). Olyankor szoktuk használni, ha egy nevet több szóból állítunk össze. Mivel a névben nem lehet szóköz, de nem is akarjuk teljesen összeragasztani a szavakat, az alulvonás nagyon megfelelő az ilyen összekötő elválasztásra.

(b) Az ábrázolható számtartomány túllépését sok Pascal megvalósítás - így a Turbo Pascal is - csak akkor jelzi, ha az ennek megfelelő parancsot vagy opciót a fordítóprogramnak megadjuk. Különösen programok fejlesztésekor érdemes ezeket az ellenőrzési lehetőségeket kihasználnunk; az értékhatárok esetleges túllépését okozó hibák korai felismerése sok bosszúságtól kímélhet meg bennünket.

(c) Az (a) megjegyzésben említettük, hogy a legtöbb megvalósítás megengedi a -maxint-1 értékű egész ábrázolását is. Mivel ennek megfelelő pozitív egész szám nincs, ilyen értékű kifejezés negálása, illetve abs függvényének kiszámítása hibát okoz. (A Turbo Pascal és a HiSoft Pascal ilyenkor nem hajtja végre a műveletet.)

(d) Példák a div műveletre:

0 div 3 = 0   -1 div 3 = 0
1 div 3 = 0   -2 div 3 = 0
2 div 3 = 0   -3 div 3 = -1
3 div 3 = 1   -4 div 3 = -1
4 div 3 = 1   -5 div 3 = -1
5 div 3 = 1   -6 div 3 = -2
6 div 3 = 2   -4 div -3 = 1
4 div -3 = -1    

(e) Csak s>=0 és n>0 esetén igaz mindig,

s mod n = s - s div n * n

egyébként csak akkor, ha a maradék nulla.

(f) Példák a mod műveletre:

0 mod 3 = 0   -1 mod 3 = 2
1 mod 3 = 1   -2 mod 3 = 1
2 mod 3 = 2   -3 mod 3 = 0
3 mod 3 = 0   -4 mod 3 = 2
4 mod 3 = 1   -5 mod 3 = 1
5 mod 3 = 2   -6 mod 3 = 0
6 mod 3 = 0    

Hibát okoz 4 mod -3, -4 mod -3 és 0 mod 0 is, mert az osztó nem pozitív.

(g) A maradékképzésnek ez a definíciója negatív osztók esetén eltér a legtöbb számítógép által használt módszertől, amely a mod műveletben is megengedi negatív osztó használatát. Ezeknél az eredmény minden nem nulla osztó esetén s-(s div n)*n, a maradék előjele így mindig megegyezik az osztandó előjelével, kivéve persze, ha a maradék 0, aminek nincs előjele.

Ha ábrázoljuk például x mod 3 értékét x függvényében, akkor a szabványos Pascalban mást kapunk, mint a legtöbb megvalósításban.
A szabványos Pascalban:

A legtöbb megvalósításban (így a Turbo Pascalban és HiSoft-Pascalban is) viszont:

Ha gépünk nem a Pascal szabványának megfelelően szolgáltatja a modulo művelet eredményét, de mindenképpen a szabványnak megfelelően akarunk eljárni, használhatjuk a következő függvényt:

function modst(s,n:integer):integer;
var w:integer;
begin
  if n>0 then begin
    w:=s-s div n*n;
    if w<0 then modst:=w+n else modst:=w
  end
  else halt
end;

Megjegyzés: a régebbi implementációk esetleg nem ismerik a halt eljárást, ami a program megszakítására szolgál. Ezek a fordítók általában elfogadják helyette a w:=1/0 utasítást, amely szintén megszakítja a program futását. A Turbo Pascal, HiSoft-Pascal viszont ez utóbbi - nyilvánvalóan hibát okozó - megoldást nem fordítja le.

(h) A szabványos Pascal nyelvben nem létezik a más programozási nyelveknél megszokott hatványozás művelet, sem egészekre, sem valósakra. Néhány megvalósításban (de a Turbo Pascalban és a HiSoft Pascalban nem) azonban bővítésként hatványozás is használható, jele általában **. Ha mégsem, egészeknél és nemnegatív kitevőre használhatjuk a következő függvényt:

function intpow(n,k:integer):integer;
var p,g:integer;
begin
  p:=1;
  for g:=1 to k do p:=p*n;
  intpow:=p;
end;

(Valós értékekre és negatív kitevőre a real típus műveleteinél láthatunk hasonló függvényt.)

(i) Talán furcsának tűnik az ord, pred és succ függvény használata egész típus esetén, hiszen ord-nak semmi hatása sincs, pred(k) és succ(k) pedig sokkal szemléletesebben fejezhető ki a k-1, illetve k+1 összeggel. Valóban, egészekre nem szoktuk ezeket a függvényeket használni. Mivel azonban ezek az összes sorszámozott típusra léteznek, és az integer a sorszámozott típusok közös alapjának tekinthető, e függvényeknek értelmezve kell lenniük az integer típusra is.

(j) Mivel az egészek belső, bináris ábrázolásában a páratlan számok utolsó, azaz legkisebb helyiértékű, jobb oldali bitje 1, az odd függvény ezen utolsó bit értékének megfelelő logikai értéket ad:

odd(n) = true,      ha az utolsó bit 1,
odd(n) = false,    ha az utolsó bit 0.


Egész számok osztásának tehát két eredménye van: a hányados egész része és a maradék; mindkettőt nagyon gyakran használjuk. A mod művelet például alkalmas arra, hogy számok oszthatóságát vizsgáljuk meg vele, hiszen n mod k akkor nulla, ha n maradék nélkül osztható k-val.

1.9. Példák

Néhány egyszerű példa következik az egészekkel végezhető műveletek és az eddig megismert utasítások használatára.

1.9.1. Példa:
Készítsünk programot, amely beolvas egy egész értéket és kiírja ennek legkisebb osztóját! Természetesen csak egynél nagyobb osztóra vagyunk kíváncsiak.

Megoldás:
A módszer lényege, hogy kettővel kezdve, egyesével felfelé haladva kipróbáljuk, milyen osztó van meg a számban maradék nélkül. Ha találunk ilyen értéket, leáll a keresés és kiírjuk a megtalált osztó értékét, ez a módszerből adódóan biztosan a legkisebb.

program legkisebb_oszto;
var szam,oszto:integer;
begin
  write('A vizsgalando szam: ');read(szam);
  oszto:=1;
  repeat
    oszto:=oszto+1;      { oszto novelese }
  until szam mod oszto=0;{ leall, ha oszthato }
  write(' legkisebb osztoja: ',oszto)
end.

A program első utasítása egy kiírás. Erre a program működéséhez nem is lenne szükség, viszont segít a program futtatásakor azzal, hogy jelzi a képernyőn, milyen adatot vár. Turbo Pascalban read(szam) helyett readln(szam) írandó.
Az oszthatóság vizsgálatát egy repeat ciklus leállási feltételébe tettük: akkor fejeződik be a ciklus, ha a szám osztható az osztóval. Az osztó növelését a ciklusmag végzi, és mert a repeat utasítás ciklusmagja legalább egyszer végrehajtódik, ahhoz, hogy az első próbánál az osztó 2 legyen, a ciklus előtt az oszto változó értékét 1-re kell beállítani. Legkésőbb akkor áll le a ciklus, ha az osztó eléri a szám értékét.

1.9.2 Példa:
Készítsünk programot, amely meghatározza egy szám összes osztóját! A vizsgálandó szám bemeneti adat. Osztónak tekintjük az 1-et és magát a számot is.

Megoldás:
Ciklusban megkeressük és kiírjuk a szám összes osztóját, egészen addig, amíg az osztó el nem éri magát a számot. Az osztót úgy keressük meg, ahogyan azt az előbbi programban láttuk:

program osszes_oszto;
var szam,oszto:integer;
begin
  write('A vizsgalando szam: ');read(szam);
  write(' osztoi :');
  oszto:=0;
  repeat
    repeat
      oszto:=oszto+1;
    until szam mod oszto=0;
    write(oszto,' ');
    until oszto=szam;
  writeln;
end.

Turbo Pascalban read(szam) helyett readln(szam) írandó.
A program érdekessége az előzőekhez képest az, hogy két repeat ciklus is van benne, mégpedig egyik a másikban, azaz az egyik repeat ciklus a másik ciklusmagjában van. Az ilyen ciklusokat egymásba ágyazott ciklusoknak nevezzük.
Nyelvi szempontból ez egyáltalán nem jelent különlegességet/hiszen a repeat és until szavak között tetszőleges utasítások lehetnek, tehát repeat utasítás is. (Későbbi ismereteinkkel - for ciklussal - ezt a feladatot egyszerűbben is meg tudjuk majd oldani.)

Természetesen az until szó az előtte legközelebb álló olyan repeat szóhoz tartozik, amelynek még nem volt until-ja.
Néhány példa következik repeat utasítások egymásba ágyazására. Az összetartozó repeat és until kulcsszavakat megjelöltük:

repeat
  ...
  repeat
    ...
  until...
  ...
until...
...
repeat
  ...
  repeat ... until...
  ...
  repeat ... until...
  ...
until...

{1. eleje }

{2. eleje }

{2. vege }

{1. vege }

{3. eleje }

{4. eleje es vege }

{5. eleje es vege }

{3. vege }

A fenti programrészletből is látható, hogy nem lehet egymást keresztező repeat ciklusokat csinálni; csak olyanokat, amelyek vagy egymás mellett vannak, vagy az egyik teljes egészében tartalmazza a másikat. Így tehát lehetetlen ilyen programrészt írni:

repeat
  ...
  repeat
    ...
until...
  ...
  until...

{1. eleje }

{2. eleje }

{1. vege }

{2. vege }

Az előbbi ábrán a tagolással (beljebb kezdéssel) azt próbáljuk sugallni, mintha az 1. repeat szó az 1. until-hoz tartozna, a második pedig a másodikhoz. A gép azonban ezt nem így veszi figyelembe. A tagolástól függetlenül mindegyik until-t az előző, még be nem fejezet repeat-hez rendeli, így itt is az 1. repeat-hez a 2. until, a 2. repeat-hez pedig az 1. until fog tartozni.
Hogy ezt nehogy eltévesszük, érdemes az eddigi programlistákhoz hasonlóan a repeat utasítás magját alkotó utasításokat valamivel beljebb kezdeni. Így az egymásba ágyazások szerkezete már ránézésre kiderül, illetve már a program írásakor látjuk, hogy minden ciklust befejeztünk-e. Ha a beljebb kezdéseket ugyanis következetesen a program szerkezetének megfelelően csináljuk - és ez nemcsak a repeat utasításra, hanem minden hasonló szerkezetre is vonatkozik, például a program utasításait határoló begin-end párra is akkor a program végén (és csakis ott) ismét a sor elejére kell visszatérnünk.

1.9.3. Példa:
Készítsünk programot, amely kiírja a beolvasott szám prímtényezős felbontását, azaz azokat a prímszámokat, amelyek szorzata a számot adja! Szokás ezt törzstényezős felbontásnak is nevezni.

Megoldás:
A módszer lényege, hogy a fentiekben megismert módon megkeressük a szám legkisebb osztóját, majd egy ciklusban addig írjuk ki és osztjuk vele a számot, amíg megvan benne maradék nélkül. Ezután megkeressük a következő legkisebb osztót, és osztogatunk vele stb. Mindezt addig kell ismételni, amíg a szám el nem fogy, tehát az osztás eredménye már 1 nem lesz.

program primtenyezok;
var szam,oszto:integer;
begin
  write('A vizsgalando szam: ');read(szam);
  write('Primtenyezos felbontasa: ');
  oszto:=1;
  repeat
    repeat
      oszto:=oszto+1;
    until szam mod oszto=0;
  repeat
    write(oszto,' ');
    szam:=szam div oszto;
   until szam mod oszto<>0
  until szam=1;
  writeln
end.

Turbo Pascalban read(szam) helyett readln(szam) írandó.

1.10. A szabványról

"- Úgy tűnt, mintha örökre szólna..."
"- Minden alkalom örökre szól..."

[0] 229. oldal

Ez az aljfejezet megadja a szabványban, tehát a nyelv szabályainak leírásában használt kifejezések jelentését,illetve a szabvány érvényességi körét.


A Pascal programozási nyelv szabványáról
A szabványos Pascal nyelv alatt az ISO szabvány szerinti programozási nyelvet értjük. [6]

1. A szabvány érvényessége:
A szabvány a Pascal programozási nyelv formai és jelentésbeli (szintaktikai és szemantikai) szabályait rögzíti: megadja a szabványos Pascal programmal és az ezt végrehajtó mechanizmussal szemben támasztott követelményeket.
A szabvány nem rögzíti a következőket:

2. Fogalmak, elnevezések, definíciók:

3. A processzorral szemben támasztott követelmények:

4. A szabványos, azaz a szabványnak megfelelő Pascal programokkal szemben támasztott követelmények:

A szabványos program ezek szerint felhasználhat megvalósításban definiált tulajdonságokat, így a program végrehajtásának eredménye különbözhet különböző szabványos processzorok használatakor.
Ezt szemlélteti a következő szabványos program:

program p;
begin
  writeln(maxint)
end.

Ennek a programnak az eredménye attól függ, hogy milyen megvalósítást használunk, hiszen maxint értéke megvalósításban definiált.

Magyarázatok, megjegyzések:

(a) A Pascal program felhasználhatóvá válhat fordítás, szerkesztés, betöltés vagy közvetlen végrehajtás (azaz interpretálás) segítségével.

(b) Hogy a megvalósításfüggő tulajdonság nem feltétlenül rögzített, az azt is jelenti, hogy nem feltétlenül szerepel a processzorhoz tartozó leírásban. Másrészt egyetlen megvalósításon belül is változhat, például a program egyes részeiben más lehet, mint a többi részben.


2. Algoritmusok egész, valós logikai és karakteres adatokkal

Az előző fejezetben megismerkedtünk a Pascal nyelv legfontosabb elemeivel és fogalmaival, és láttunk néhány mintaprogramot is. Ahhoz, hogy ezeknél összetettebb feladatokat is meg tudjunk oldani, további nyelvi eszközökre, további utasításokra és adattípusokra is szükségünk van.
Az utasítások közül a kétirányú elágazást lehetővé tevő if, a többirányú elágazást végző case, az utasítások egy sorozatát egyetlen utasítássá összefogó utasításcsoport, a begin...end, valamint további két ciklusutasítás, a while és a for következik. Az új adattípusok a valós, azaz real, a logikai, azaz boolean és a karakteres, azaz char típusok lesznek.

2.1. A valós (real) típus

2.1.1. Példa:
Készítsünk programot, amely beolvas három számot, majd kiszámítja és kiírja ezek átlagát!

Megoldás:
A feladat nem egész számról, hanem egyszerűen számról beszél, ezért a programot úgy kell elkészíteni, hogy ne csak egészekkel lehessen használni.
Törtszámok tárolását, a törtekkel való műveletvégzést a valós (angolul: reai, kiejtése: ríl) adattípus teszi lehetővé. A valós típus ezen kívül az egészeknél lényegesen nagyobb, illetve 1-nél lényegesen kisebb abszolút értékű számok használatát is lehetővé teszi. A valós adattípus azonosítója: real.
A valós konstansokat a zsebszámológépeknél megszokott módon decimális számjegyekkel adjuk meg, az egészrészt a törtrésztől tizedespont választja el, a tíz hatványaként értelmezett kitevőt pedig akár e, akár E betű után írhatjuk. (A HiSoft Pascal csak a nagy E betűt fogadja el valós számban.)
Néhány valós konstans:

-1.2
3.14159
3.87E6
1E-6

Az átlag kiszámításának módszere nagyon egyszerű - a három szám összegét el kell osztani hárommal:

program harom_szam_atlaga;
var a,b,c,atlag:real;
begin
  write('Kerem a harom szamot: ');read(a,b,c);
  atlag:=(a+b+c)/3;
  write('A harom szam atlaga = ',atlag)
end.

Turbo Pascalban read(a,b,c) helyett readln(a,b,c) írandó.
A / műveleti jel a valós osztást jelenti, ennél tehát tört értékű eredmény is keletkezhet, maradék nincs.
Újdonság az is, hogy egyetlen read utasítással 3 változó értékét olvassuk be. Ebben az esetben - amely egyébként kerülendő - a megfelelő számú értéket legalább egy szóközzel elválasztva kell megadnunk.
Ebben a programban a real típuson és a / műveleten kívül az is újdonság, hogy zárójeleket használunk az értékadó utasításban. Erre azért van szükség, mert ha egy kifejezésben több különböző műveleti jel is van, esetünkben összeadás és osztás, akkor a gép a matematikában megszokotthoz hasonlóan először a szorzást és az osztást, utána pedig az összeadást és a kivonást végzi el. Ha a programban az állag kiszámításakor nem használnánk zárójelet, tehát azt írnánk, hogy

atlag:=a+b+c/3;

akkor az atlag értéke

a+b+(c/3)

lenne. Ha a programot Turbo Pascalban futtatva a -2 4.3 81e2 bemeneti adatokkal futtatjuk le, akkor eredményül azt kapjuk, hogy

A harom szam atlaga = 2.70076666667E+03

azaz a gép a számot kitevős alakban jeleníti meg. Más megvalósításokban az eredmény pontosabb, vagy épp (pl. a HiSoft Pascal-ban) kevésbé pontos.

2.1.2. Példa:
Készítsünk programot, amely tetszőleges számú szám átlagát határozza meg!

Megoldás:
Az első megoldandó probléma az, hogy a program megírásakor még nem tudjuk, hány szám átlagát kell meghatároznunk. Ez azt jelenti, hogy egy ciklusban kell beolvasnunk és összeadnunk a számokat.
Az egyik lehetséges megoldás az, hogy először beolvassuk a számok darabszámát, azaz, hogy hány szám átlagát akarjuk meghatározni, majd ciklusban ennyiszer olvasunk be és adunk az eddigi összeghez egy- egy számot.
A program általános szerkezete:

számok számának beolvasása
számláló és összeg nullázása
ciklus az egyes számok feldolgozására:
   következő szám beolvasása,
   összegzés
    számok számlálása
ciklus vége
átlag kiszámítása és kiírása

Most a program működését nem Pascal utasítások, hanem egyszerű mondatok vagy pár szavas megjegyzések formájában adtuk meg. Ezt a módszert egy-egy algoritmus felvázolására később gyakran fogjuk használni.
A megfelelő Pascal program:

program szamok_atlaga;
var szamok,szamlalo:integer;
    szam,osszeg:real;
begin
  write('Szamok szama: ');read(szamok);
  szamlalo:=0;osszeg:=0;
  repeat
    szamlalo:=szamlalo+1;  { szamlalas }
    write(szamlalo,'. szam: ');read(szam); { beolvasas }
    osszeg:=osszeg+szam;   { osszegzes }
  until szamlalo=szamok;
  writeln('A beolvasott szamok atlaga =',osszeg/szamok);
end.

Turbo Pascalban read utasítás helyett readln írandó.

Stílus:
Bár a Pascal programozási nyelv utasításai önmagukban is elég jói olvashatóak - legalábbis a nyelvet ismerők számára -, érdemes meghagyni az előzetes algoritmusterv szövegét is, mégpedig kommentek, azaz megjegyzések formájában. Így a program megírásával egyidejűleg elkészülnek a megjegyzések is. Ezeknek - mint erről már volt szó - nagy jelentőségük van a program későbbi olvashatósága, és így módosíthatósága, azaz karbantarthatósága szempontjából. Másrészt sok programozó gondolkozik úgy, hogy:

"Először gyorsan megírom a programot, azután ha majd készen leszek, ellátom a megfelelő megjegyzésekkel."

Ez meglehetősen gyakori tévhit. Ha már megírtuk a programot, nem szívesen bíbelődünk a megjegyzések utólagos beírásával, így a program megjegyzések nélkül marad. Ez kellemetlen, mert programunk rosszul olvasható lesz, és ha a program nem csak pár sorból áll, akkor a megírása is nehezebb az eligazodást segítő megjegyzések nélkül.
A most vizsgált program még olyan egyszerű és rövid, hogy megjegyzések nélkül is könnyen érthető volna. Ennek ellenére érdemes már az egyszerű programoknál hozzászokni a megfelelő kommentezéshez.

A programnak több érdekessége is van.
Az első, hogy kétféle adattípussal is találkozunk, az egész és a valós típussal. A változók deklarációjánál, azaz itt a var és a begin kulcsszavak között külön kellett felsorolni a különböző típusú változókat, a felsorolás végén pedig a típus azonosítója, real, illetve integer áll.
Egy programban tetszőleges számú adattípust használhatunk, a változók deklarációjának sorrendje tetszőleges. Így az előzővel azonos lenne akár a

var szam,osszeg:real; szamok,szamlalo:integer;

deklaráció, az áttekinthetőség kedvéért azonban érdemes az összetartozó adatokat együtt megadni.

A program második érdekessége, hogy a write-tal nem egy változó, hanem egy kifejezés értékét írjuk ki:

writeln('A beolvasott szamok atlaga =',osszeg/szamok);

Ilyenkor a gép először meghatározza a kifejezés értékét, és az így kapott értéket jeleníti meg.

A harmadik érdekesség, hogy az átlag kiszámításakor a write-ban a / jel bal oldalán valós, jobb oldalán pedig egész adat van. Maga a / művelet valós számok osztását végzi, azaz mindkét operandusa valós mennyiség kell legyen. Mivel itt a jobb oldali operandus egész típusú, ezt az osztás elvégzése előtt a gép valóssá alakítja és így végzi el a műveletet.

Egy programozási feladatnak több jó megoldása is lehet, így ezt a programot is megírhatjuk másképpen.
Előfordulhat, hogy kellemetlen lenne előre megszámolni, hány szám átlagát akarjuk meghatározni. Ha például kiscsibék átlagos testsúlyára vagyunk kíváncsiak, az előző program használatához először meg kellene számolni őket. Ez majdnem ugyanannyi munka, mintha egyúttal meg is mérjük a súlyukat: egyenként át kell raknunk őket az egyik tárolóból a másikba, mert az eredeti helyükön a nagy nyüzsgés miatt biztosan eltévesztenénk a számlálást.

2.1.3. Példa:
Alakítsuk át az előbbi programot úgy, hogy ne kelljen előre ismerni az átlagolandó számok számát!

Megoldás:
Természetesen ekkor is kell jelezni a programnak, hogy nincs több adat, tehát kiléphetünk a beolvasás és összegzés ciklusából. Használjuk fel a kiscsibék súlyának azt a tulajdonságát, hogy nem lehet nulla, azaz egy nulla bemeneti adat jelezze, hogy nincs több átlagolásra váró érték. Az átlagot persze a beolvasott értékek összegéből és számából tudjuk meghatározni, ezért továbbra is szükség van az adatok számlálására.
A program általános szerkezete:

számláló és összeg nullázása
ciklus az egyes számok feldolgozására:
   következő szám beolvasása, összegzés
   számok számlálása
ciklus vége, ha a szám nulla
átlag kiszámítása és kiírása

alig változott az eredeti változathoz képest, ezért a program is nagyon hasonló:

program szamok_atlaga_2;
var szamlalo:integer;
    szam,osszeg:real;
begin
  szamlalo:=0;osszeg:=0;
  repeat
    szamlalo:=szamlalo+1;
    write(szamlalo,'. szam: ');read(szam);
    osszeg:=osszeg+szam;
  until szam=0;
  writeln('A beolvasott szamok atlaga =',osszeg/(szamlalo-1))
end.

Egy lehetséges bemeneti adatsorozat például: 5.6 -6.1 3 5 0 vagyis akár negatív érték is használható, csak nulla nem.
A programhoz egyetlen magyarázat kell még: miért szamlalo-1-gyel osztottunk, és nem szamlalo-val? Ennek az az oka, hogy a ciklusban a lezáró nulla beolvasásakor is megnöveljük a számláló értékét, azaz abban a tényleges értékek számánál eggyel nagyobb szám lesz a ciklus befejezésekor. Ezt megoldhatjuk úgy is, hogy a számlálónak nem 0, hanem -1 kezdeti értéket adunk, ekkor valóban a számláló értékével kell végül osztani.

Tipikus hiba:
Nagyon gyakori, hogy egy ciklusban eggyel többször vagy éppen eggyel kevesebbszer hajtunk végre valamit, mint ahogyan kellene, annak ellenére, hogy a ciklussal kapcsolatos minden más - esetleg meglehetősen bonyolult - dolgot helyesen végzünk el. Érdemes ezért a ciklusokat az első fejezetben már megismert kézi módszerrel néhány egészen egyszerű esetre kipróbálni.
Ebben a példában ilyen egyszerű eset lehet például, amikor egyetlen tényleges bemeneti adat van. Ekkor az osszeg először ez az érték lesz, hozzáadódik a lezáró 0, a ciklusból kilépve számláló értéke 2, tehát a program jónak tűnik.

Gyakran használjuk a fenti programban alkalmazott módszert ismeretlen számú adat beolvasására: az utolsó tényleges adat után olyan értéket adunk meg, amely maga már nem lehet bemeneti adat, a beolvasási ciklust ennek hatására fejezzük be. Ha bemeneti adatként a nulla is megengedett érték, akkor a beolvasási ciklust például egy negatív értékkel lehet leállítani. Ha azonban tetszőleges, azaz pozitív, negatív és nulla értéket is meg kell engednünk bemeneti adatként, akkor ez a módszer már nem alkalmazható, vagy legalábbis nem ilyen egyszerűen.


A valós (real) típus:

1. A valós adattípus a matematikából racionális számként ismert mennyiségek valamilyen pontossággal való ábrázolására alkalmas, lehetővé teszi törtrész és tízes kitevő használatát is.
Előre definiált, azaz szabványos típus, előre definiált típusazonosítója real, értékkészlete és pontossága megvalósításfüggő. (a) (b)

2. A valós számok írásmódjának formai szabályai:

előjeles valós szám:

előjel nélküli valós szám:

ahol a törtrész előjel nélküli egész szám, a kitevő előjeles egész szám.

Ha van törtrész, akkor az egészrészt és a törtrészt tizedespont választja el egymástól. A tizedespont előtt is és után is legalább egy számjegy áll. Ha van kitevő, akkor az egy E vagy e betű után áll (HiSoft-Pascalban E betű). Jelentése az, hogy a szám értéke a kitevő előtt álló érték szorozva a 10 kitevőre emelt hatványával. Egy valós számban a tizedespont és a kitevő közül legalább az egyiknek szerepelnie kell.
Például helyes formátumú valós számok:

1.2
-6E3
+0.8
1.0
-63.82e-12

Hibás valós számok:

12 egész, tehát nem valós szám, a megfelelő valós szám pl. 12.0 vagy 1. 2E1 vagy 12E0
.5 nincs számjegy a tizedespont előtt, a helyes alak: 0.5
32. nincs számjegy a tizedespont után, helyesen: 32.0
36 E5 szóköz van a kitevőrész előtt, helyette jó pl.: 36E5 vagy 3.6E+6

3. Valós adattal a következő műveletek végezhetők:

4. Kifejezésekben valós számokkal az alább következő műveletek végezhetők.

Hibát okoz, ha valamelyik művelet eredménye nem esik az adott megvalósításban a valósakra megengedett értéktartományba. A műveletek a nekik megfelelő matematikai műveletek bizonyos pontosságú közelítései, a közelítés pontossága megvalósításban definiált. Az operandusok valósak.

5. A valós számok előre deklarált függvényei az alábbiak.

 

Példák:

trunc(3.5) =  3
trunc(-3.5) = -3
trunc(2) =  2
trunc(-2) = -2

Példák:

round(3.1) =  3
round(-3.1) = -3
round(3.5) =  4
round(-3.5) = -4

 

6. Ha egy kifejezésben olyan helyen szerepel egész érték, ahol valósnak kellene lennie, az értéket a gép automatikusan átalakítja valóssá, és azzal végzi et a műveletet. (g)
Ez a következő esetekben fordulhat elő:

Magyarázatok, megjegyzések:

(a) A valós típusnál nincs olyan előre definiált konstans, mint az egészeknél a maxint,de a megvalósítás hivatkozási kézikönyvei meg szokták adni az ábrázolható legnagyobb és legkisebb abszolút értékű valós számot.
A legkisebb ábrázolható pozitív valós értéket - felhasználva, hogy a számítógépek a számokat kettes számrendszerben tárolják - a következő függvénnyel határozhatjuk meg például:

function minreal:real;
var p,r:real;
begin
  r:=1;
  repeat
    p:=r;r:=r/2;
  until r=0;
  minreal:=p
end;

A függvény csak olyan megvalósításban fut le, amely nem végez automatikus túlcsordulás ellenőrzést (pl. Turbo Pascal).
Az előző függvényben fontos, hogy 2-vel osszunk és ne 0.5-del szorozzunk, mert a tizedestörtek bináris belső alakra alakításakor kerekítési pontatlanságok adódhatnak.

A legnagyobb ábrázolható valós értéket program segítségével nem tudjuk ilyen kényelmesen meghatározni, hiszen amikor átlépjük a legnagyobb ábrázolható értéket, az túlcsorduláshibát okoz, tehát a program futásának megszakítását jelenti. Így mindössze annyit tehetünk, hogy egy ciklusban minden lépésben kiírjuk a duplázott valós értéket, és a túlcsordulás előtt kiírt utolsó érték adja a legnagyobb ábrázolható valós számot. Azért adunk duplázáskor 1-et is az összeghez, mert duplázáskor a szám utolsó bináris számjegye 0 lenne, 1 hozzáadásával tehát mindig valóban a lehető legnagyobb értéket kapjuk:

program maxreal;
var r:real;
begin
  r:=1;
  repeat
    r:=r+r+1;
    writeln(r);
  until false { vegtelen ciklus }
end.

A real típus értékkészletét a számegyenesen így ábrázolhatjuk:

(b) A számítógép a valós értékeket véges hosszúságú bináris szám formájában tárolja. A belső ábrázolást általában a következő módon jellemezhetjük:

e * m * 2^k

ahol e az előjelet adja meg, értéke +1 vagy -1 lehet;
  m az úgynevezett mantissza, legtöbbször normalizált érték: 0. 5<=m<1.0 .Ennek a pontossága, vagyis az m tárolására használt bitek száma adja meg a valós szám ábrázolásának pontosságát;
  k a bináris kitevő.

Adott megvalósítás valós ábrázolási pontosságát és a mantissza bitjeinek számát - ha ezt a megvalósítás kézikönyve nem adná meg - a következő programmal tudjuk meghatározni:

program realpontossag;
var r:real;
    b:integer;
begin
  r:=1/2;b:=0;
  repeat
    r:=r/2;b:=b+1;
  until 1+r=1;
  { leall a ciklus, ha az 1+r erteket a gep 1.0-ra kerekiti }
  writeln('Relativ pontossag: ',2*r);
  writeln('Mantissza bitjeinek szama: ',b);
  writeln('Ertekes decimalis szamjegyek szama legfeljebb:',
           trunc(-ln(r+r)/ln(10)))
end.

Mivel a valós számok ábrázolási pontossága véges, az értékkészletet megadó előbbi számegyenesen a megengedett értékek tartományát valójában nem folytonosnak, hanem szaggatottnak kellene jelölnünk.

A szabványos Pascalban nincs hatványozás, de néhány megvalósításban (de a Turbo Pascalban és a HiSoft Pascalban nem) a ** jellel végezhetünk hatványozást. Ennek a műveletnek elsőbbsége van a multiplikatív, tehát a szorzás és osztás műveletekkel szemben is, azaz ezeknél előbb értékelődik ki. Ha mégsincs ilyen az általunk használt megvalósításban, és programunkban többször is szükségünk lenne rá, a valós értékű halvány kiszámítására deklarálhatunk például ilyen függvényt:

function Pow(a:real;x:integer):real;
begin
  if x=0 then pow:=1
  else if x>0 then
    pow:=a*Pow(a,x-1)
  else
    pow:=1/Pow(a,-x)
end;

(Turbo Pascalban használnunk kell a {$A-} fordítási direktívát.)

Figyelembe véve a 6. szabályból ismert automatikus típusátalakítást, ezt a függvényt egészeit negatív kitevős hatványozása esetén is használhatjuk, természetesen valós eredményt kapva.
Például:

v:=pow(5,-6)

ahol v valós változó.

Nagyon vigyáznunk kell valósok egyenlőségének vizsgálatával. Ha ugyanis a vizsgálatban szereplő valamelyik operandus értéke számítás eredménye, a számítási pontatlanságok és kerekítések miatt a két valós szám értéke gyakorlatilag sohasem egyenlő.
Próbáljuk ki a következő egyszerű ciklust!

r:=0
repeat
  r:=r+0.1;
writeln(r);
until r=1 { sohasem teljesül! }

Ez a programrészlet gyakorlatilag végtelen ciklusban fog futni, ugyanis az egyenlőség 0.1 ábrázolásának pontatlansága és az összeadásnál végzett kerekítés miatt sohasem teljesül. Miután r értéke túllépi az 1.0 értéket, a ciklust csak r túlcsordulása állíthatná le. Ez akkor is csak nagyon hosszú futás után következne be, ha a 0.1 hozzáadása során nem kellene kerekíteni. (A kor mikroszámítógépei bináris lebegőpontos formátumban tárolták a valós számokat, amiben a 0.1 nem ábrázolható pontosan. Az Enterprise IS-BASIC, és a TVC BASIC viszont BCD formátumban tárolja a számokat, ezek pontosan tudnak számolni tizedekkel.)
A (b) megjegyzésben, a relatív pontosság meghatározásánál r értéke lett olyan kicsi 1.0-hez képest, hogy összeadás után, a számábrázolás véges pontossága miatt a kerekítés áldozata lett. Itt pedig r értéke lesz olyan nagy 0.1-hez képest, hogy a hozzáadott 0.1-et a kerekítés lenyeli. A kerekítés eredményeként tehát r értéke egy adott értéken túl már nem növekszik, így végtelen ciklusba kerülünk.
Két valós érték ezért gyakorlatilag csak akkor lehet egyenlő, ha egyik a másiktól, vagy mindketten valamilyen közös őstől, például ugyanolyan alakú konstanstól kapták értéküket.

(e) Az arctan függvénnyel kiszámíthatjuk pi (pi=-3.14159...) értékét:

writeln('pi=',4*arctan(1))

(f) A szabvány eredeti meghatározása szerint a trunc függvény pozitív vagy nulla r esetén olyan egész értéket állít elő, amelyre
   0 <= r-trunc(r) < 1
negatív r esetén
   -1 < r-trunc(r) <= 0.

Hasonló a round szabványbeli definíciója: pozitív és nulla r esetén round(r) azonos trunc(r+0.5)-del, negatív r-nél pedig trunc(r-0.5)-del.
Az 5. pontban megadott szabályok a szabvánnyal azonos értékűek, viszont remélhetőleg könynyebben érthetőek.

(g) Az egész-valós az egyetlen automatikus típusátalakítás a Pascal nyelvben, az összes többi átalakítást a programban külön meg kell adnunk megfelelő függvényekkel.

(h) Változóparaméterek esetén az automatikus átalakítás azért nem végezhető el, mert ott nem csak egészvalós irányba, hanem fordítva is el kellene végezni az átalakítást.


2.2 Az if utasítás

"Minden döntés, amelyre el kellett magát szánnia,
újabb döntéshez vezetett, s ez megint döntést vont maga után."

[0] 242. oldal

Olyan eset is van, amikor a szamok_atlaga_2 programunk nem működik helyesen: ha mindjárt az első beolvasott érték nulla, akkor a ciklus csak egyszer fut le, a számláló értéke 1 lesz, a gép nullával próbál osztani, és ez hibát okoz. Ilyenkor ibaüzenetet kapunk a géptől, mely arra figyelmeztet, hogy nullával nem lehet osztani.
Ha megengednénk, hogy a program futása a gép hibaüzenetével álljon le olyan bemeneti adat esetén, amire a számítás a rendes módon nem végezhető el, az a programok használatakor nagyon kellemetlen lenne. A legtöbb üzenet meglehetősen általános jellegű ugyanis, ami nem sokat mond a program felhasználójának, aki általában nem is ismeri a program belső működéséi és így nem tudhatja, mit kell tennie, hogy a program rendesen lefusson. Ráadásul a hibaüzenetek legtöbbször angolul jelennek meg, esetleg csak egy hibakód, azaz a hiba száma íródik ki, a hiba szövegét valamilyen kézikönyvből kell kikeresni.
Jobb, ha a program maga figyeli a lehetséges hibaeseteket, és a felhasználó számára is érthető hibaüzeneteket ír ki. Ezt komoly programoktól mindig elvárhatjuk.

Esetünkben az átlag kiírása előtt meg kellene vizsgálni, hogy a számláló egyenlő-e 1-gyel, és ha igen, mondjuk a következő üzenetet kellene kiírni: "Nem volt egyetlen adat sem!", egyébként pedig az eddigi módon kell számolni.
Az ennek megfelelően átalakított program:

program szamok_atlaga_3;
var szamlalo:integer;
    szam,osszeg:real;
begin
  szamlalo:=0;osszeg:=0;
  repeat
    szamlalo:=szamlalo+1;
    write(szamlalo,'. szam: ');read(szam);
    osszeg:=osszeg+szam;
  until szam=0;
  if szamlalo=1 then
    writeln('Nem volt egyetlen beolvasott adat sem!')
  else
    writeln('A beolvasott szamok atlaga =',osszeg/(szamlalo-1))
end.

Turbo Pascalban read(szam) helyett readln(szam) írandó.
Ebben a változatban az if utasítás újdonság. Az if utasításhoz három új kulcsszó tartozik:

if = ha (kiejtése: if)
then = akkor (kiejtése: den)
else = egyébként (kiejtése: elsz)

Az if utasítás végrehajtása meglehetősen egyszerű: a gép először megvizsgálja az if szó utáni feltételt. Ha ez teljesül, akkor a then utáni utasítást, ha nem teljesül, akkor pedig az else utánit hajtja végre. A then és az else után is csak egyetlen utasítás állhat. Ha a then és az else szó között több utasítás is van, az szintaktikai hibát jelent. Ha pedig az else után van több utasítás, akkor ezek közül a gép csak az elsőt tekinti az if utasítás részének, az else utáni második utasítást már úgy veszi, hogy az a teljes if utasítás után van, tehát az if-beli feltételtől függetlenül mindig végrehajtja.
Konkrét esetben, programunkban, ha szamlalo=1, akkor kiírja azt a hibaüzenetet, ami a program felhasználója számára is érthető, és ezzel a teljes if utasítás be is fejeződik.
Ha számláló értéke nem 1, akkor az else után az eredmény kiírása következik.

Az eddig megismert nyelvi elemek segítségével már összetettebb programok is írhatók.

2.2.1. Példa:
Készítsünk egy programot, amely bármilyen bemeneti adat esetén helyesen számítja ki az

r*x = s

egyenlet megoldását; x az ismeretlen, r és s pedig bemeneti adat.

Megoldás:
A feladat első ránézésre egyszerűnek tűnik, hiszen az egyenlet átrendezésével ezt kapjuk:

x = s/r

Mivel ezt az átrendezést csak akkor tehetjük meg, ha r nem nulla, az r=0 esetet külön meg kell vizsgálni. Ha ekkor s is nulla, akkor minden x megoldása az egyenletnek, hiszen az egyenlet

0*x = 0

alakú. Ha pedig r=0 esetén s értéke nem nulla, akkor nincs megoldása az egyenletnek, hiszen

0*x = nemnulla

alakú, és ez semmilyen x-re sem teljesül.
A feladatot megoldó program:

program egyismeretlenes_egyenlet;
var r,s:real;
begin
  write('Az  r*x=s  egyenlet r es s adata: ');read(r,s);
  if r=0 then
    if s=0 then write('Az egyenletnek minden szam megoldasa.')
    else write('Az egyenletnek nincs megoldasa!')
  else write('Az egyenlet megoldasa: x=',s/r:5:2)
end.

Turbo Pascalban read(r,s) utasítás helyett readln(r,s) írandó.
Az előző gondolatmenet alapján a program működése könnyen követhető. Csak egy újdonság van: az if utasítás then ágában egy másik if utasítás van. Ezt természetesen megtehetjük, hiszen akár a then, akár az else után tetszőleges utasítás jöhet, akár if utasítás is.

Stílus:
Érdemes megfigyelni a program tagolását, azaz a sorokra bontás és a bekezdések használatát. Ezzel is kiemelhetjük a program szerkezetét, az egyes utasítások egymásba ágyazását. A program megírható lenne ilyen tagolás nélkül is, de akkor szerkezete lényegesen nehezebben lenne követhető, annak ellenére, hogy a program nem is bonyolult.


Az if utasítás:

1. Alakja:

2. Az if utasítás végrehajtása: ha van else-ág, akkor ez két alternatíva közötti választást, ha nincs else-ág, akkor egyetlen utasítás végrehajtását vagy végre nem hajtását vezérli:

Az elmondottak folyamatábrája:

3. A then és az else szó után is csak egy-egy utasítás állhat, az azonban tetszőleges utasítás lehet. Ha bármelyik helyen több utasítást is meg akarunk adni, akkor ezeket a begin és end kulcsszavakkal utasításcsoportba kell összefognunk.

4. Az egymásba ágyazott if utasításoknál előfordulhat a következő szerkezet:

if logkif1 then if logkif2 then ut1 else ut2;

Itt első ránézésre úgy tűnhet, nem tudjuk eldönteni, hogy az egyetlen else-ág a belső vagy a külső if utasításhoz tartozik. Erre az esetre a következő szabály érvényes: az else-ág mindig az előtte álló legközelebbi else nélküli if utasításhoz tartozik. (b)(c)
Így az előbbi szerkezetben az else-rész a belső if-hez tartozónak tekintendő, tehát a programrész folyamatábrája:

Magyarázatok, megjegyzések:

(a) Az else-ág hiányát az jelzi, hogy a then-ág utasítása után nem az else kulcsszó következik, hanem valami más olyan nyelvi eszköz, amely egyértelműen jelzi a then-ág utasításinak befejeződését. Ez tipikusan a pontosvessző (;), de ezen kívül az end vagy az until kulcsszó is lehet.
Az end kulcsszó akkor következhet a then-ág után, ha az if utasítás egy utasításcsoportban szereplő utasítássorozat utolsó eleme. Hasonlóképpen, until kulcsszó akkor lehet a then-ág után, ha az if utasítás egy repeat ciklus utasítás-sorozatának utolsó eleme.
Akár az end, akár az until kulcsszó jelzi az else-ág hiányát, előtte ilyen esetben is állhat egy (elvileg felesleges) pontosvessző:

begin
   ...
   if ... then ... ; { felesleges ; }
end

Ez lényegében azt jelenti, hogy a then-ágat, és így az egész if utasítást lezáró pontosvessző és az until, illetve end között egy üres utasítás áll.
Az előzőekből egy fontos szabály következik: else előtt soha nincs pontosvessző! Ez ugyanis nemcsak a then-ág utasításának befejezését, hanem az egész if utasítás végét is jelentené, hiszen ha lenne else-ág, akkor a then utáni utasítás végét az else kulcsszó jelezné.

(b) A 4. szabályt akár az előzőekből is kikövetkeztethetjük, ha átgondoljuk a Pascal fordítóprogram működését az

if logkif1 then
  if logkif2 then ut1
  else ut2

szerkezet feldolgozásakor. (Itt a jobb áttekinthetőség kedvéért bekezdésekkel tagolva ismételtük meg a 4. pontbeli programrészletet.)
A fordító először a külső if utasítás feldolgozásához fog hozzá: az if utasítást feldolgozó programrész hívódik meg, ez lefordítja a logkif1 kifejezést, majd elér a then részhez. Mivel a then után utasításnak kell állnia, a fordító meghívja az egy utasítást lefordító programrészt. Ez beolvassa a mi programunk következő szimbólumát: if - tehát a then-ág utasítása egy if utasítás lesz. Így az utasításfordító programrész meghívja az if utasítást lefordító programrészt. Ez lefordítja logkif2-t, megtalálja a belső then kulcsszót, lefordítja az ut1 utasítást, majd továbblép: itt vagy else ágnak kell lennie, vagy valami olyan szimbólumnak, amely azt jelzi, hogy nincs else-ág. A mi esetünkben valóban else kulcsszó következik, tehát a belső if utasítást feldolgozó programrész lefordítja az else-ágat, beleértve az ut2 utasítást is. Ezzel a belső if utasítást feldolgozó programrész befejeződik, azaz visszatér az őt hívó programrészbe, az utasítás-fordítóba, és ez szintén visszatér az őt hívó, külső if utasítást fordító programrészbe. Ez ott tart, hogy befejezte a then-ág utasításának fordítását, most megvizsgálja, hogy van-e else-ág. Mivel a következő szimbólum a pontosvessző, azt találja, hogy nincs else-ág, vagyis a külső if utasítás fordítása is befejeződött. Vegyük észre, hogy az előbbi gondolatmenetből az derül ki, hogy a fordító az egyetlen else-ágat a belső if utasítás részének tekintette.
Ha a belső else-ágat akarjuk elhagyni az egymásba ágyazott if utasításokból, akkor erre három lehetőségünk is adódik:

Ez a megoldás talán természetesebbnek, olvashatóbbnak is tekinthető.

Azért nem teljesen ekvivalens ez a megoldás az előzőekkel, mert itt logkif2 esetleg akkor is kiértékelésre kerül, ha logkif1 hamis. Ez nem mindig engedhető meg, például azért, mert ha a logkif1 feltétel hamis, akkor az első változatban logkif2 nyugodtan hivatkozhat meghatározatlan értékű változókra, ebben a változatban pedig nem.
A másik különbség az, hogy itt logkif1-et kétszer is kiértékelheti a gép. Ez esetleg sok művelet ismételt elvégzését jelenti, pl. ha logkif1 függvényhívást is tartalmaz. Még az is lehet, hogy a két kiértékelés más-más eredményt ad, ha például a logkif1 kifejezés értéke véletlen értéktől is függ.


Az if utasítás a gyakorlatban lényegesen egyszerűbben, magától értetődőbben használható, mint aztaz előbbi szabály- és következtetéssorozat alapján vélnénk. Elvileg elég lett volna csak a legfontosabb szabályokat ismertetni, a tapasztalat azonban azt mutatja, hogy sokszor egy-egy "érthetetlen" hiba megoldásához is nagyon hasznos a szabályok következményeinek, esetleg a szabályok belső logikájának részletes ismertetése.

2.2.1. Feladat:
Készítsen programot, amely a beolvasott számról eldönti, hogy prímszám-e, és ha nem az, akkor kiírja a prím osztóit.

2.2.2. Feladat:
Készítsen programot, amely beolvas egy pozitív egész számot, majd kiírja az összes prímszámot eddig a számig! Mérje meg a program futási idejét, ha a bemeneti adat 10, 100, 1000, illetve 10000.

2.3. Függvények használata

2.3.1 Példa:
Készítsünk programot az

ax^2 + bx + c = 0

egyenlet valós megoldásainak megkeresésére! A bemeneti adatok az a, b és c együtthatók. Készítsük el a programot úgy, hogy bármilyen bemeneti adat esetén jól működjön!

Megoldás:
A másodfokú egyenlet megoldására általában az úgynevezett gyökképletet szoktuk használni:

Mint az előző példában már láttuk, a feladat megoldása nem egyszerűen a megoldóképlet alkalmazását jelenti, hiszen ahhoz, hogy a program mindig jól működjön, további vizsgálatokra is szükség van. Mivel itt az a, b és c együtthatók értékeitől függően sok eset lehetséges, érdemes a programot először szöveges formában megtervezni:

együtthatók beolvasása
ha nem másodfokú, akkor
   az elsőfokú egyenlet megoldása,
egyébként
   ha a diszkrimináns negatív, akkor
      nincs valós megoldás,
   egyébként
      a valós megoldások kiszámítása és kiírása

A megoldás vázlatát tovább finomíthatjuk azzal, hogy az elsőfokú egyenlet megoldását részletesen leírjuk:

együtthatók beolvasása
ha nem másodfokú, akkor
   az elsőfokú egyenlet megoldása:
      ha x együtthatója nulla, akkor
         ha a jobb oldal nulla, akkor
            minden szám megoldás,
         egyébként
            nincs megoldása,
      egyébként a megoldás kiírása
   egyébként
      ha a diszkrimináns negatív, akkor
         nincs valós megoldás,
      egyébként
         a valós megoldások kiszámítása és kiírása

A részletes terv alapján a teljes program:

program masodfoku_egyenlet;
var a,b,c,d,x1,x2:real;
begin
  writeln('ax^2 + bx + c = 0');
  write('a masodfoku egyenlet a, b es c egyutthatoi: ');
  read(a,b,c);    { egyutthatok }
  if a=0 then     { az egyenlet b*x=-c alaku }
    if b=0 then   { az egyenlet 0*x= c alaku }
      { az egyenlet 0*x=0 alaku }
      if c=0 then write('Minden valos szam megoldas!')
      else write('Nincs megoldas!')
    else write('Egyetlen valos megoldas van: ',-c/b)
  else begin      { valodi masodfoku egyenlet }
    d:=b*b-4*a*c; { diszkriminans erteke }
    if d<0 then write('Nincs valos megoldas.')
    else begin    { van valos megoldas }
      x1:=(-b+sqrt(d))/(2*a); { megoldasok }
      x2:=(-b-sqrt(d))/(2*a);
      writeln('Ket valos megoldas van: ');
      writeln('  x1= ',x1,'   x2= ',x2)
    end
  end
end.

Turbo Pascalban read(a,b,c) helyett readln(a,b,c) írandó.
A programban az eddig ismertekhez képest két új dolgot is találunk, mégpedig az sqrt függvényt, és a már említett begin...end kulcsszavak eddigiektől eltérő módon való használatát.

Függvényekkel már találkoztunk az egész és valós számok tulajdonságainak összefoglalójában, használatukra azonban még nem volt szükségünk.
Az sqrt függvény olyan nyelvi elem, amely a függvény zárójelében megadott érték négyzetgyökét (angolul:, square root, kiejtése: szkver rút) számítja ki. A programunkban például sqrt(d) a d változó értékének négyzetgyökét adja.
A függvény hívása nem önálló utasítás, hanem csak egy értéket ad, ezért mindig kifejezésben használjuk. Olyan kifejezést, amely függvényhívást tartalmaz, bárhol használhatunk, ahol kifejezés állhat. Az előző példában egy értékadó utasítás jobb oldalán használtuk, de lehet ilyen kifejezés például feltétel vizsgálatában is.
Azt a mennyiséget, amit a függvény neve után zárójelben megadunk, azaz aminek a függvényét a gép kiszámítja, a függvény paraméterének nevezzük. A paraméter értékét nemcsak változóval, hanem megfelelő kifejezéssel is megadhatjuk, például:

sqrt(11.4)
sqrt(w*4/tt-6.8)
sqrt(e-1+sin(0.2)/3.3)

Az első példában konstans értékének négyzetgyökét kerestük. A harmadikban az sqrt függvény paraméterében egy további függvény, a sin is szerepel, ez a trigonometriából ismert szinuszfüggvényt jelenti.
Függvény értékének kiszámításakor a gép először meghatározza a paraméter értékét, majd ebből az adott függvénynek megfelelő értéket.

A függvényeket két nagy csoportba sorolhatjuk, vannak előre deklarált, más néven szabványos (standard) függvények, és a programban a programozó is deklarálhat függvényeket.
A szabványos függvényeknek vagy nincs paraméterük, vagy csak egy van. A szabványos függvények nevét, jelentését, a paraméter típusát és megengedett értéktartományát a függvény típusának megfelelő adattípus összefoglaló leírásában soroltuk fel. Az sqrt és a sin szabványos függvények, paraméterük és értékük típusa is valós. Az sqrt függvény például csak pozitív érték vagy nulla négyzetgyökét képes meghatározni, az ln függvény pedig, amely a paraméter természetes logaritmusát adja meg, csak pozitív paraméterrel használható. Ha a paraméter értéke nem megfelelő, akkor a program végrehajtása általában megszakad és valamilyen barátságtalan hibaüzenetet kapunk. Ezt érdemes a paraméter értékének előzetes ellenőrzésével elkerülni.
A programunkban d értékét a négyzetgyökvonás előtt megvizsgáltuk, így itt nem történhet baj.

Ha programban definiálunk függvényt, akkor annak tetszőleges számú, akár 33 vagy nulla paramétere is lehet. Ez a szám egy adott függvény esetén azonban rögzített, és hogy az adott függvénynek hány és milyen típusú paramétere van, azt a függvény deklarációjában adjuk meg.
Ha a függvénynek nincs paramétere, akkor a zárójeleket sem kell kiírni a függvény neve után.

A diszkrimináns kiszámítását írhattuk volna így is:

d:=sqr(b)-4*a*c;

azaz használhattuk volna az sqr négyzetre emelés (angolul: square, kiejtése: szkver) szabványos függvényt is. Egyszerűbb volt b*b-t írni a függvény hívása helyett, de ha összetettebb kifejezés, például

(z-3)*(f+1)-q

négyzetét kell kiszámítani, akkor kényelmesebb azt írni, hogy

sqr((z-3)*(f+1)-q)

mint azt, hogy

((z-3)*(f+1)-q)*((z-3)*(f+1)-q)

Arról nem is szólva, hogy az utóbbi változat kiszámítása több időbe is kerül, hiszen a gép először külön-külön meghatározza a zárójeles kifejezések értékét, majd ezek eredményét szorozza össze, azaz közel kétszer annyit számol.

Rövid összefoglaló az előre deklarált függvényekről ábécérendben:

Függvény neve Paraméter rípusa Eredmény típusa Jelentése
abs integer vagy real mint a paraméter abszolút érték
arctan real real árkusz tangens
chr integer char a paraméternek megfelelő ord értékű karakter
cos real real koszinusz
eof adatállomány boolean adatállomány vége
eoln text boolean sor vége
exp real real e (=2.71828...) alapú hatvány
ln real real e (=2.71828...) alapú logaritmus
odd integer boolean páratlan
ord sorszámozott integer sorszám
pred sorszámozott mint a paraméter előző érték
round real integer kerekítés
sin real real szinusz
sqr integer vagy real mint a paraméter négyzetre emelés
sqrt real real négyzetgyökvonás
succ sorszámozott mint a paraméter következő érték
trunc real integer csonkítás

HiSoft-Pascal-ban nincs file típus, így eof függvény sincs.

2.4. Az utasításcsoport: begin...end

A másodfokú egyenletet megoldó programban kétszer is előfordul, hogy az if utasítás else-ágában több utasítást is meg kell adni. Az if utasításban azonban a then és az else után is csak egy-egy utasítás lehet, tehát olyan nyelvi eszközre van szükségünk, amely két vagy több utasítást egyetlen utasítássá fog össze, azaz utasítás-zárójelként működik. Erre használható az utasításcsoport nevű utasítás, amelyet két kulcsszó határol:

begin = kezdet (kiejtése: bigin)
end = vég (kiejtése: end)

A begin és end között tetszőleges számú utasítást megadhatunk egymástól pontosvesszővel elválasztva, ezek a programkörnyezet számára egyetlen utasítást jelentenek.
A begin és end szavakkal egyébként már találkoztunk, hiszen minden program utasításrésze is ilyen utasításcsoport.
Utasításcsoportot használhatunk akkor is, ha az if utasításban a feltétel teljesülésekor akarunk több dolgot végrehajtani, azaz a then szó után akarunk több utasítást írni:

if ... then begin
   ...
end
else ...

Vannak még más olyan Pascal utasítások is, amelyekben egy bizonyos helyen csak egyetlen utasítás állhat. Ezeknél szükség esetén szintén lehet utasításcsoportot használni.


Az utasításcsoport

1. Alakja:

2. Az utasításcsoport utasítás a begin és end kulcsszavak közötti utasításokat egyetlen utasítássá fogja össze. (a)(b)(c)

3. Végrehajtása: a gép a benne szereplő utasításokat a megadás sorrendjében hajtja végre.

Magyarázatok, megjegyzések:

(a) Ezt az utasítást elsősorban ott használjuk, ahol a nyelv szabályai szerint csak egyetlen utasítás lehetne, mégis több utasítást akarunk végrehajtani.
Tipikusan ilyen az if, a case, a while, a for és a with utasítás.
Utasításcsoport ezeken kívül a blokkok utasításrésze is.

(b) Ezt az utasítást a magyar nyelvű szakirodalom gyakran "összetett utasítás"-nak nevezi. Mi ezt az elnevezést általánosabb fogalom megjelölésére használjuk: összetett utasításnak számítanak mindazok az utasítások, amelyek további utasításokat tartalmazhatnak: repeat, while, for, if, case, with utasításcsoport. Az utasításcsoport a többi összetett utasítástól eltérően csak egyetlen dolgot végez: utasítás-zárójelként egyetlen utasítássá fogja össze a benne felsorolt utasításokat.

(c) Mivel a Pascal nyelvben üres utasítás is létezik, elképzelhető, hogy az end szó előtt közvetlenül egy pontosvessző áll:

if ... then begin
   ut1;
   ut2;
   ...
   utn;
end
else ...

Bár ez feleslegesnek tűnik, természetesen nem hibás. Úgy tekinthető, mintha a pontosvessző után egy üres utasítás állna. Ez a felesleges pontosvessző néha hasznos is lehet: ha a program módosítása során például egy vagy több további utasítást szúrunk be az end elé, ha nem volt ott ez a "felesleges" pontosvessző, szinte biztosan el fogunk feledkezni róla utólag.


Végül még egy megjegyzés a programunkhoz. A másodfokú egyenlet valós megoldásainak kiírását egy sorba így is végezhetnénk:

writeln('Ket valos megoldas van: ',
        '  x1= ',x1,'   x2= ',x2);

azaz a kiírásba az aposztrófok között megadott szöveget is úgy osztottuk két sorba, hogy először egy aposztróffal lezártuk az első részt, majd a következő sorban a második részt külön aposztrófok között adtuk meg, tehát az aposztrófok közötti szöveg nem nyúlt át egyik sorból a másikba. Azért használtunk két külön, aposztrófokkal határolt és egymástól vesszővel elválasztott szövegrészt, mert aposztrófok között nem kezdhetünk új sort. Ez a megoldás hasznos lehet, ha nagyon hosszú kiíró utasítást használunk.

"... vannak dolgok, amiket nem lehet gondolkodással kideríteni,
csak tapasztalat útján. "

[0] 236. oldal

Néhány feladat az eddigi ismeretek gyakorlására:

2.4.1. Feladat:
Olvassunk be két valós számot és írjuk ki őket növekvő sorrendben, azaz először a kisebbiket, utána a nagyobbikat!

2.4.2. Feladat:
Ugyanaz, mint a 2.4.1 feladat, de három számmal.

2.4.3. feladat:
Készítsünk programot, amely nullától 360 fokig 10 fokonként kiszámítja és kiírja a szögek szinuszát és koszinuszát!
(Vigyázzunk: a sin és cos függvények számára a szöget radiánban kell megadni!)

2.4.4. Feladat:
Készítsünk programot, amely a beírt egész számokról eldönti és kiírja, hogy melyik prímszám és melyik nem! Nem tudjuk előre, hogy hány számot akarunk megvizsgálni.
(Prímszámnak azokat az egész számokat tekintjük, amelyek csak eggyel és önmagukkal oszthatók maradék nélkül.)
A feladat megoldását szöveges programvázlat készítésével kezdjük!

2.4.5. Feladat:
Készítsünk programot, amely beolvassa egy háromszög három oldalát és ezekből meghatározza, majd kiírja a háromszög területét!
A megoldáshoz használhatjuk a Héron-képletet:

ahol t a háromszög területe, a, b és c a háromszög oldalai, s pedig a háromszög kerületének fele.
Írjuk meg úgy a programot, hogy tetszőleges bemeneti adatokkal működjön!

2.4.6. Feladat:
Olvassunk be egy dátumot (év, hónap, nap), majd írjuk ki, hogy lehetséges-e ez a dátum, és ha nem, akkor mi a hiba benne!

2.4.7. Feladat:
Számítsuk és írjuk ki az első n Fibonacci-számot, n legyen bemeneti adat!
A nulladik és első Fibonacci-szám rendre 0 és 1, a továbbiakat úgy kapjuk, hogy az őt megelőző kelt összeadjuk. Pl.:
F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(6)=8.

2.4.8. Feladat:
Olvassunk be egész számokat, és írjuk ki, hogy közülük hányadik volt a legkisebb, illetve legnagyobb, és mennyi ezek értéke. A számok pozitívak, negatívak és nulla értékűek is lehetnek. Nem ismerjük előre a számok darabszámát, de azt tudjuk, hogy egyik sem lehet nagyobb 30000-nél.
Ellenőrizzük programjaink működését kézi módszerrel, ahogyan azt az első programnál láttuk!

2.5. Összetett logikai feltétel: and, or és nőt művelet

2.5.1. Példa:
Készítsünk egy programot, amely beolvassa a tanulók osztályzatait, és kiszámítja ezek átlagát! A program egyetlen futtatásával tetszőleges számú átlagszámítást lehessen elvégezni. Az összetartozó osztályzatok végét olyan szám jelezze, amely nem lehet osztályzat, azaz nem 1 és 5 közé esik. A teljes program akkor fejeződjön be, ha egy osztályzatcsoport nulla darab osztályzatot tartalmaz.
Például egy lehetséges bemeneti adatsorozat (külön sorba írva a 4 osztályzatcsoportot):

5 5 5 5 5 5 5 5 0
2 4 3 5 5 6
1 2 1 4 4 3 -3
1 2 3 4 5 5 4 3 2 1 9
0

Megoldás:
A program általános szerkezete:

ciklus az osztályzatcsoportokra:
   összeg, osztályzatszámláló nullázása,
   osztályzatok ciklusa:
      osztályzat beolvasása,
      ha lehet ilyen osztályzat, akkor
            összegzés, számlálás,
      ciklus vége, ha nem lehet ilyen osztályzat
      ha volt osztályzat a csoportban, akkor
         az átlag kiszámítása, kiírása
osztályzatcsoport ciklus vége, ha nulla darab osztályzat volt

A terv alapján a programot majdnem teljes egészében meg tudjuk írni, hiszen már oldottunk meg hasonló feladatot.
Az egyetlen problémát az okozza, hogy hogyan vizsgáljuk meg, lehet-e osztályzat a beolvasott szám. Ehhez ugyanis két feltételnek kell egyszerre teljesülnie: az osztályzat legalább 1, de legfeljebb 5 lehet. Az eddig megismert nyelvi eszközökkel ezt - nem éppen áttekinthető módon - így oldhatnánk meg (feltételezzük a használt változók megfelelő deklarációját):

...
if osztalyzat>=1 then
  if osztalyzat<=5 then begin
    osszeg:=osszeg+osztalyzat;
    szamlalo:=szamlalo+1
  end;
...

Az összegzésre és számlálásra csak akkor kerül sor, ha mindkét if utasítás feltétele teljesül. Ezzel azösszegzést és a számlálást megoldottuk, viszont az osztályzatok repeat ciklusában az until utáni feltételbe nem írhatunk ilyen kettős if szerkezetet.
Az egyik lehetséges megoldás az, hogy egy további változóban tároljuk a feltételvizsgálat eredményét, és ezt használjuk a repeat ciklusfeltételében is:

repeat
  read(osztalyzat);
  if osztalyzat>=1 then { lehet ilyen osztalyzat? }
    if osztalyzat<=5 then begin { ... igen, lehet }
      osszeg:=osszeg+osztalyzat;
      szamlalo:=szamlalo+1;lehet:=1
    end
    else lehet:=0 { osztalyzat>5 }
  else lehet:=0   { osztalyzat<1}
until lehet=0;

A megoldás lényege tehát az, hogy akkor összegzünk és számlálunk, ha az

osztalyzat>=1 és az osztalyzat<=5

feltétel is teljesül, illetve a ciklusból akkor lépünk ki, ha az

osztalyzat<1 vagy az osztalyzat>5

feltételek bármelyike teljesül.

A program megírásához tehát új nyelvi eszközökre van szükségünk. Ezeknek egyszerű logikai, azaz igen-nem feltételeket kell összekapcsolniuk úgy, hogy ha kell, az így keletkező teljes feltétel az egyszerű feltételek mindegyikének, ha kell, akkor pedig azok bármelyikének teljesülésekor legyen igaz. Erre az and és az or műveleteket használhatjuk, az and magyar jelentése "és", az or jelentése "vagy". (Kiejtésük: end, ill. or.)

Az and és or műveletnevek is kulcsszavak, hasonlóan a div, illetve mod szavakhoz.
Két operandusuk van, ezek logikai típusúak és az elvégzett művelet eredménye is logikai.

Az and művelet eredménye csak akkor igaz, ha mindkét operandusa igaz; vagyis ha bármelyik operandusa hamis, az and művelet eredménye is hamis.

Az or művelet eredménye akkor igaz, ha legalább az egyik operandusa igaz; vagyis csak akkor hamis, ha mindkét operandusa hamis.

a b a and b a or b
igaz igaz igaz igaz
igaz hamis hamis igaz
hamis igaz hamis igaz
hamis hamis hamis hamis

A programozásban használt or művelet jelentését a magyar nyelvben az "akár" szóval talán jobban viszsza lehet adni, mint a "vagy"szóval. Hiszen az or műveletnek akár az egyik, akár a másik operandusa igaz, az eredménye is az; míg a vagy szót a magyarban gyakran olyankor használjuk, ha két lehetőség közül csak az egyik teljesülhet, mintegy kizárva a másikat: "Vagy megszoksz, vagy megszöksz". Szokjuk meg, hogy a programozásban a vagy szót mindig az előbbi, táblázat szerinti jelentésben használjuk! A kizárólagos választást egyes programnyelvekben a „kizáró vagy” művelettel lehet kifejezni, és bár a Pascalban nincs kifejezetten ilyen logikai művelet, itt ugyanezt adja a logikai mennyiségek <> művelettel való összehasonlítása.
Az and műveletet tehát akkor használjuk, ha egy feltétel teljesüléséhez a két összetevő feltétel mindegyikének teljesülnie kell; az or műveletet pedig akkor, ha egy feltétel teljesüléséhez a két összetevő közül az egyik teljesülése is elég.

A Pascalban ezeken kívül még egy logikai művelet létezik, a tagadás, vagy más szóval a negálás művelet. Ennek egyetlen operandusa van, eredménye pedig az operandus logikai negáltja: ha az operandus igaz, akkor a not művelet eredménye hamis, ha pedig az operandus hamis, akkor az eredmény igaz. Táblázattal:

a not a
igaz hamis
hamis igaz

A not műveletet tehát ott használjuk, ahol egy feltétel teljesüléséhez egy másik feltétel nem teljesülése szükséges.

Ahhoz, hogy az összetett logikai feltételeket helyesen tudjuk felírni, meg kell még ismernünk ezek kiértékelési sorrendjét is, vagyis azt, hogy milyen sorrendben történik olyan összetett kifejezések kiértékelése, amelyek különböző logikai és aritmetikai műveleteket is tartalmaznak.
A kiértékelés sorrendjének megadásánál a "precedencia" kifejezést szoktuk használni, ami magyarul elsőbbséget jelent. A magasabb precedenciájú művelet elsőbbséget élvez, azaz előbb értékelődik ki, mint az alacsonyabb precedenciájú; az azonos precedenciájúakat pedig balról jobbra számítja ki.
Az eddig megismert műveletek elsőbbségi sorrendjét egy olyan táblázattal adjuk meg, amelyben az azonos precedenciájú (a kiértékelés sorrendjében egyenrangú) műveletek egy sorban szerepelnek, a sorok pedig csökkenő precedencia szerint követik egymást, azaz az első sorban a legmagasabb, az utolsóban pedig a legalacsonyabb precedenciájú műveletek vannak:

not negálás
*  /  div  mod  and multiplikatív (szorzó) műveletek
+  -  or additív (összeadó) műveletek, előjelek
=  <>  <  >  <=  >= relációs (összehasonlító) műveletek

A műveletek ilyen precedenciája miatt összetett logikai kifejezésben nagyon gyakran kell zárójelet használni. Tegyük fel például, hogy a, b, c és q egész vagy valós változó, ekkor az

a>b and c=q

kifejezés hibás, hiszen az and művelet magasabb precedenciájú, mint a > vagy az = művelet, a gép először az and műveletet próbálja végrehajtani. Az and két oldalán azonban most nem logikai, hanem egész vagy valós mennyiségek vannak, ezeknek pedig nem lehet logikai and kapcsolatát venni. A hibát kijavíthatjuk, ha zárójelek használatával előírjuk a gépnek, hogy először az összehasonlításokat végezze el:

(a>b) and (c=q)

Az and és or műveletek felhasználásával a példaprogram:

program atlagok;
var osztalyzat,osszeg,szamlalo:integer;
begin
  repeat { osztalyzatcsoportok ciklusa }
    osszeg:=0;szamlalo:=0; { nullazas }
    writeln('Osztalyzatok:');
    repeat { osztalyzatok ciklusa }
      read(osztalyzat);
      if (osztalyzat>=1) and (osztalyzat<=5) then begin
        osszeg:=osszeg+osztalyzat; { osszegzes }
        szamlalo:=szamlalo+1 { szamlalas }
      end
    until (osztalyzat<1) or (osztalyzat>5);
    if szamlalo>0 then
      writeln('Az atlag= ',osszeg/szamlalo:5:2);
  until szamlalo=0
end.

Turbo Pascalban read(osztalyzat) helyett readln(osztalyzat) írandó.
A writeln-ben a :5:2 azt jelenti, hogy az eredményül kapott értéket 5 karakter hosszúságban ki kiírni, ebből a törtrész 2 számjegyére vagyunk kíváncsiak. Néhány ilyen formában kiírt érték:

-24.25
55.00
-1,36

Érdemes megfigyelni, hogy az if utasítás és a belső repeat utasítás feltétele egymás ellentéte. Ezért a belső repeat utasítás feltételét írhattuk volna így is:

until not((osztalyzat>=1) and (osztalyzat<=5))

vagyis az if utáni feltétel negáltan ismételve.

Mivel a not a legmagasabb precedenciájú művelet, és a teljes kifejezés negáltját akartuk kiszámítani, a kifejezést zárójelbe kellett tenni.
A programban a feltétel megfordítására más módszert alkalmaztunk: ellentétes relációs jeleket használtunk, és ezek eredményét and helyett or művelettel kapcsoltuk össze. A módszer helyességét a felírt logikai feltételek vizsgálatából is látjuk, hiszen egy szám akkor lehet érvényes osztályzat, ha legalább 1, és legfeljebb 5, illetve akkor nem lehet osztályzat, ha kisebb, mint 1 vagy nagyobb, mint 5. A feltételek ilyen megfordíthatóságát általánosan is megfogalmazza a két De Morgan-féie azonosság, ezek a Pascal nyelv jelöléseivel a következők:

not (f and g) = not f or not g

illetve

not (f or g) = not f and not g

ahol f és g logikai mennyiséget jelöl.

2.5.1 Feladat:
Készítsen programot, amely megkeresi és kiírja az első 100 szám közül a prímeket, tehát azokat az egész számokat, amelyek 1-en és önmagukon kívül mással nem oszthatóak maradék nélkül!

2.5.2 Feladat:
Ha másképp írta volna meg, gyorsítsa az előbbi feladat megoldását a következő módszerekkel:

2.6. A logikai (boolean) típus

Az előbb látott megoldásban a feltétel negált megismétlése helyett hatékonyabb lenne, ha először egy vál-tozóba tennénk a beolvasott szám vizsgálatának eredményét, majd ezt, illetve ennek negáltját használnánk fel a két feltétel vizsgálatában. Ehhez olyan változóra van szükségünk, amely logikai értéket tud tárolni. A Pascal nyelvben logikai mennyiségeket boolean típusú változókban tárolhatunk, a boolean típus, az integer és a real típushoz hasonlóan, előre definiált típus. Az előbbi átlagszámító programunk átalakítva:

program atlagok2;
var osztalyzat,osszeg,szamlalo:integer;
    lehet:boolean;
  begin
  repeat { osztalyzatcsoportok ciklusa }
    osszeg:=0;szamlalo:=0;
    writeln('osztalyzatok:');
    repeat { osztalyzatok ciklusa }
      read(osztalyzat);
      lehet:=(osztalyzat>=1) and (osztalyzat<=5);
      if lehet then begin
        osszeg:=osszeg+osztalyzat;
        szamlalo:=szamlalo+1
      end
    until not lehet;
    if szamlalo>0 then
      writeln('az atlag= ',osszeg/szamlalo:5:2)
  until szamlalo=0
end.

Turbo Pascalban read(osztalyzat) helyett readln(osztalyzat) írandó.


A logikai (boolean) típus

1. A logikai adattípussal az "igaz", vagy a "hamis" értéket lehet ábrázolni.
Előre definiált, azaz szabványos típus.
boolean az előre definiált típus azonosítója.
false és true értékeket vehet fel, ahol false és true előre definiált, logikai tlpusú konstansok: false jelentése "hamis", true jelentése "igaz" (ejtsd: fólsz, trú)

2. Sorszámozott típus:

ord(false) = 0
ord(true) = 1

3. Logikai típusú adattal a következő műveletek végezhetők:

4. Kifejezésekben logikai mennyiségekkel az alábbi műveletek végezhetők.
A műveletek eredménye is logikai típusú.

5. Logikai adatok függvényei. A g-vel jelzett paraméter logikai típusú.

Magyarázatok, megjegyzések:

(a) Logikai típusú változó értékét nem lehet text típusú állományból read, illetve readln utasítással beolvasni.
Ha mégis szükségessé válik, hogy egy logikai értéket olvassunk be például a szabványos input állományból, ezt a következő eljárással tehetjük meg:

procedure readboolean(var b:boolean);
var ch:char;
begin
  repeat
    read(ch)
  until ch<>" ";
  b:=ch in ['I','i','Y','y','T','t']
end;

Ezt az eljárást például így hívhatjuk meg:

readboolean(log)

ahol az aktuális paraméterként megadott log logikai változó akkor lesz igaz értékű, ha a szabványos input állományban a következő nem-szóköz karakter az Y y I i T t karakterek valamelyike; egyébként hamis értéket kap. Ezek a betűk a Yes, Igen, Igaz, True szavak kezdőbetűit reprezentálják.

(b) Logikai változóval a következő for ciklusok készíthetők:

for L:=false to true do ...

illetve

for L:=true downto false do ...

Természetesen a kezdeti és végérték lehet logikai értékű kifejezés is.

(c) Nagyon ritkán használjuk a logikai típust case utasítás szelektoraként, hiszen

case a>b of
   true: utasítás1;
   false: utasitás2;
end

helyett előnyösebb, mert rövidebb, egyszerűbb és természetesebb az

if a>b then utasítás1 else utasítás2

szerkezet. A Pascal nyelv ezt azért engedi meg mégis, mert a boolean is sorszámozott típus.

(d) A logikai típusnál nincs az ord-nak olyan inverz függvénye, mint a char típusnál a chr A 0 ill. 1 érték logikaivá alakítását elvégezhetjük egyszerűen az n>0 összehasonlítással, amely n=1 esetén igaz, n=0 esetén pedig hamis értéket ad; vagy ha az áttekinthetőség kedvéért függvényt akarunk használni, így deklarálhatjuk az ord logikai inverzét:

function bool(n:integer):boolean;
begin
  if (n>1) or (n<0) then halt
  else bool:=n>0
end;


A sorszámozott típus fogalma
Ezzel a kifejezéssel már az egész típus szabályainál is találkoztunk.


A sorszámozott típus

1. A sorszámozott típus nem önálló adattípust jelent, mint például az egész vagy valós típus, hanem az adattípusok egy csoportját. Ide tartozik az egész (integer), a logikai (boolean), valamint) később ismertetendő karakteres (char) és felsorolt típus. (a)

2. A sorszámozott típusú változók diszkrét, azaz egyesével felsorolható értékeket vehetnek fel, és így értékkészletük leképezhető az egész számokra. Minden sorszámozott típusra értelmezett az ord függvény, amely éppen ezt a leképezést végzi el: megadja, hogy adott sorszámozott értékhez melyik egész érték tartozik, tehát mennyi a "sorszáma".

3. A sorszámozott típusokra használható még a pred és a succ függvény is. Ez nem egész értéket ad, hanem mindig olyan típusút, amilyen a paramétere, méghozzá éppen az eggyel kisebb, illetve nagyobb ord értékűt, ha ez létezik egyáltalán; ha nem létezik, az hibát okoz.

4. A sorszámozott típusú adatokkal végezhető műveletek:

Magyarázatok, megjegyzések:

(a) Nem sorszámozott típus a már ismert valós (real), a később következő mutató és egyik összetett adattípus sem.


Az itt felsorolt közös tulajdonságokat az egyes sorszámozott típusok tárgyalásakor is megadjuk.

Stílus:
A legutóbbi, átlagszámító programból láthatjuk, hogy boolean típusú változóba egy logikai kifejezés értékét köz?vetlenül, egyetlen értékadó utasítással eltehetjük. Elvileg jó, de bonyolultsága miatt mégis kerülendő a következő megoldás:

if (osztalyzat>=1) and (asztalyzat<=5) then lehet:=true
else kehet:=false;

Különböző aritmetikai és logikai műveleti jelek felhasználásával meglehetősen összetett kifejezéseket is kialakíthatunk. Eddig is használtunk különböző típusú, például egész, valós, illetve logikai kifejezéseket, ezek felírásának legtöbb szabályát már ismerjük. A kifejezések használatának általános szabályait a következő rész foglalja össze.


Kifejezés

1. A kifejezés egy értéket ad meg, ennek kiszámítási módja a kifejezésben használt mennyiségektől és műveletektől függ.

2. A formai szabályok:

kifejezés:

egyszerű kifejezés:

tag:

tényező:

3. A műveleti jelek precedenciája, azaz elsőbbségi sorrendje a legmagasabbtól a legalacsonyabb felé haladva:

not negálás
*  /  div  mod  and multiplikatív, azaz szorzó műveletek
+  -  or additív, azaz összeadó műveletek, előjelek
=  <>  <  >  <=  >= relációs, azaz összehasonlító műveletek

Ha egy kifejezésben több különböző precedenciájú művelet is szerepel, akkor a gép először a legnagyobb precedenciájút hajtja végre, majd a következő legnagyobb precedenciájút stb., végül pedig a legkisebb precedenciájút.
Kettő vagy több egymást követő, azonos precedenciájú művelet esetén balról jobbra haladva értékeli ki a kifejezést, azaz először a legbaloldalibb művelet hajtódik végre, majd az ettől jobbra lévő stb., végül a legjobboldalibb.
Például:

5*3 div 4

ugyanazt jelenti, mint

(5*3) div 4

tehát értéke 3.

A kifejezésben szereplő tényezők értékének meghatározási sorrendje megvalósításfüggő, tehát a szabály nem rögzíti, hogy az f1*f2 kifejezésben, ahol f1 és f2 is függvény, melyikük végrehajtása történik meg előbb. (c)
Az is lehetséges, hogy a kifejezés egy részét a gép nem is értékeli ki. (d)

4. Ha van olyan változó a kifejezésben, amelynek az értéke a kifejezés kiértékelésekor meghatározatlan, hiba történik.
Ez akkor is így van, ha a változó értéke nem befolyásolja a kifejezés értékét, pl. 0*a

Magyarázatok, megjegyzések:

(a) Figyeljük meg, hogy amíg tényezőket multiplikatív operátorokkal, illetve tagokat additív operátorokkal összekapcsolva tetszőleges számban adhatunk meg egymás után, addig egyszerű kifejezésekből zárójelezés nélkül legfeljebb kételemű kifejezést állíthatunk össze, a kifejezés szintaktikai diagramja ugyanis nincs visszahurkolva. Így tehát megengedettek a következők:

a*b*c div d mod 3*(p*q)
p+q-r+z*c-b

de hibás:

a>b=c<d
a=b=c

Helyettük, ha ezt a használt azonosítók típusa megengedi, használható például:

(a>b)=(c<d)
(a=b)=c

mert a zárójellel az egyes részkifejezéseket tényezőkké alakítottuk.

(b) A változóhivatkozás különböző lehetőségeit, az egyes műveletekben megengedett adattípusokat valamint az egyes műveletek jelentését és használatát a megfelelő adattípusoknál részletezzük.

(c) Egy példa annak bemutatására, hogy az f1*f2 kifejezés értéke függhet f1 és f2 kiértékelésének sorrendjétől:

...
var g:integer;
...
function f1:integer;
begin g:=1; f1:=g; end;
function f2:integer;
begin g:=g+2; f2:=g; end;
...
begin
  g:=0; write(f1*f2);
end.

Ha f1 értékelődik ki először, akkor az előbbi programrészlet az 1*3=3 értéket írja ki, ha f2, akkor pedig a 2*1=2 értéket.

(d) Egy kifejezés valamely operandusának kiértékelése akkor maradhat el, ha a kifejezés értéke nem függ a kérdéses operandus értékétől. Például az

(a>b) or (c-1=0)

kifejezésben a>b teljesülése esetén a teljes kifejezés értéke true lesz, az or művelet jobb oldali operandusának (c-1=0) értékétől függetlenül.
Előfordulhat, hogy egyes megvalósítások hasonló esetekben valóban nem értékelik ki a kifejezés egyes részeit.Ha az általunk használt megvalósítás ilyen, akkor sem érdemes ezt a tulajdonságot kihasználni, mégpedig a programok biztonságos hordozhatósága érdekében, azaz, hogy más gépen, illetve más Pascal-megvalósítással is módosítás nélkül használhatók legyenek.


Eddig háromféle adattal, adattípussal találkoztunk: integer, reál és boolean. A kifejezésekben ezeket vegyesen használhatjuk, de tudnunk kell, vajon tetszőleges módon keverhetők-e a különböző típusú adatok a kifejezésekben, illetve értékadó utasításokban.
Nos, a Pascal nyelv ebből a szempontból is nagyon következetes: minden adattípus csak a neki megfelelő műveletekben használható, és egy változónak csak a típusának megfelelő érték adható. Ezt úgy is fogalmazhatjuk, hogy a Pascal nyelvben nem létezik - a sok más nyelvben megszokott - automatikus típuskonverzió, kivéve egyetlen magától értetődő esetet: ha kell, az egész értéket a gép valóssá alakítja. Az összes többi típusátalakítást a megfelelő függvénnyel nekünk kell előírni (pl. trunc, round, ord).
Ez a megkötés a program megbízhatóságát növeli, hiszen a kifejezések kiértékelésekor nem történhet olyan művelet, amit a programozó nem írt elő. Ha a gép automatikusan alakítana át például valós értéket egésszé, akkor ez félreértésre adna lehetőséget, hiszen ezt az átalakítást kétféleképpen is el lehet végezni.
Az adattípusok tárgyalásakor - mint már láttuk - mindig foglalkozunk az adott adattípussal végezhető műveletekkel, illetve azzal, hogy milyen előre definiált függvények állnak rendelkezésünkre a típusok közötti átalakításokhoz.

2.7. A while utasítás

Atlagok2 nevű programunk kicsit nehézkes, mert két helyen is tekintetbe kell venni, hogy az adott osztályzat megengedett értékű-e: az összegzéskor és a ciklus leállási feltételében is. Nézzük meg egy osztályzatcsoport beolvasásának és összegzésének ciklusát folyamatábrán és rendezzük át!

Egy osztálycsoport ciklusa
Egy osztálycsoport ciklusa átalakítva

A második változatban a beolvasott érték vizsgálata csak egyszer szerepel, bár a beolvasást nemcsak a ciklusban, de egy alkalommal a ciklusba lépés előtt is el kell végezni. Ez a változat az előbbinél áttekinthetőbbnek, egyszerűbbnek tűnik, viszont megvalósításához olyan ciklusutasításra van szükségünk, amely nem a ciklusmag végrehajtása után vizsgálja meg a ciklusfeltételt, mint a repeat utasítás, hanem a ciklusmag előtt.
A while ciklusutasítás pontosan ezt teszi.


A while utasítás

1. Alakja:

2. Az utasítás végrehajtása: a gép először kiértékeli a logikai kifejezést. Ha ez hamis, akkor a while utasítás végrehajtása befejeződik, azaz a program végrehajtása a teljes while utasítást követő utasítással folytatódik.
Ha a logikai kifejezés értéke igaz, akkor a gép végrehajtja a do után álló egyetlen utasítást, majd ismét a while szó utáni kifejezést értékeli ki stb. Ez tehát azt jelenti, hogy amíg a logikai kifejezés, azaz a ciklusfeltétel igaz, a gép ciklikusan végrehajtja a do után álló utasítást.
Ezt úgy is fogalmazhatjuk, hogy a while után álló logikai kifejezésben a ciklus folytatásának feltételét kell megadnunk, hiszen addig folytatódik a ciklus, amíg ez a feltétel teljesül.

3. A do kulcsszó után csak egyetlen utasítás állhat. Ha mégis több utasítást akarunk végrehajtani, akkor ezeket az utasításokat a begin...end kulcsszavakkal utasításcsoporttá kell összefognunk.

Magyarázatok, megjegyzések:

(a) A feltétel kiértékelése itt - a repeat utasítással ellentétben - a ciklusmagot jelentő utasítás végrehajtása előtt történik meg, ezért a feltételben szereplő értékeket a while utasításba lépés előtt megfelelően be kell állítani.

(b) Ha a logikai kifejezés már a while utasításba lépéskor sem teljesül, akkor a do utáni utasítás egyszer sem hajtódik végre.

(c) A ciklusmag lehet üres is; ennek tipikus alakja:

while feltétel do;

Ilyenkor magának a feltételvizsgálatnak a végrehajtása kell olyan változást okozzon, aminek hatására ez a feltétel egyszer majd hamisra változik. Ez csak akkor lehetséges, ha a feltétek függvényt hívunk.


Az átlagszámító program while utasítást tartalmazó változata:

program atlagok3;
var osztalyzat,osszeg,szamlalo:integer;
begin
  repeat { osztalyzatcsoportok ciklusa }
    osszeg:=0;szamlalo:=0;
    writeln('osztalyzatok: ');read(osztalyzat);
    while (osztalyzat>=1) and (osztalyzat<=5) do begin
      osszeg:=osszeg+osztalyzat; { osszegyes }
      szamlalo:=szamlalo+1; { szamlalas }
      read(osztalyzat)
    end;
    if szamlalo>0 then
      writeln('Az atlag= ',osszeg/szamlalo:5:2)
  until szamlalo=0
end.

Turbo Pascalban read(osztalyzat) helyett readln(osztalyzat) írandó.

2.8. Egy ravasz feladat

Az eddigiek alapján úgy tűnhet, hogy a kifejezések kialakítása, illetve a feltételek megfogalmazása nem okozhat különösebb nehézséget. A most következő mintapélda azt hivatott érzékeltetni, hogy bizonyos viszonylag egyszerűnek tűnő feladatok esetén is alaposan át kell gondolni, mit is jelentenek az egészek értékkészletére, illetve a kifejezések kiértékelésének sorrendjére vonatkozó szabályok.

2.8.1. Példa:
Készítsünk programot, amely egész számokat olvas be és összegzi ezeket! A beolvasott számsor végét jelezze 0 érték. A program álljon le és adjon megfelelő hibaüzenetet, ha a számok összege túllépné az egészekre megengedett értéktartományt.

Megoldás:
Első gondolatunk, hogy ez nagyon könnyen megoldható az eddigiek alapján, hiszen egy ciklusban kell beolvasni és összegezni az egész számokat, és a ciklust be kell fejezni, ha 0 érkezett, vagy ha az összeg túllépné a megengedett értéket:

until (szam=0) or (osszeg>maxint) or (osszeg<-maxint);

A hiba maxint fogalmának alapos félreértéséből adódik, ugyanis maxint azt jelenti, hogy egyetlen egész szám abszolút értéke sem lehet nagyobb nála, így az összeg ilyen vizsgálata értelmetlen.
Az első ötletünk talán az, hogy maxint túllépésének vizsgálatát az összeg következő értékének kiszámítása előtt kell elvégezni:

...
repeat
  read(szam);
  hiba:=(osszeg+szam>maxint) or (osszeg+szam<-maxint);
  if not hiba then osszeg:=osszeg+szam
until (szam=0) or hiba;

Itt gyakorlatilag ugyanazt a hibát követtük el, mint az előző esetben. Teljesen mindegy ugyanis, hogy az összeadás eredményét először eltesszük-e egy változóba és azután vizsgáljuk meg ezt az értéket, vagy az összeget vizsgáljuk, és csak akkor tároljuk, ha úgy tűnik, hogy helyes. Ennek az az oka, hogy a vizsgálathoz itt is ki kell számítani először osszeg+szam értékét, és ha az így kapott részeredmény nagyobb lenne, mint maxint, akkor máris hiba történt.
Ha mégis lefuttatjuk az előbbi két programot, egyes gépeken azt tapasztaljuk, hogy ha az összeg túl naggyá vagy túl kicsivé válik, a gép hibaüzenetet ad erről és megszakítja a program végrehajtását (pl. HiSoft-Pascal). Az is előfordulhat, hogy sohasem kapjuk meg "Az összeg túl nagy" üzenetet, és a gép elég furcsa módon maxint pozitív irányú túllépésekor néha negatív, míg -maxint negatív irányú túllépésekor pozitív összegértéket ír ki (pl Turbo Pascal). Az a szerencsésebb eset, ha a program leáll egy hibaüzenettel, hiszen legalább azonnal látjuk, hogy hiba történt, míg az utóbbi esetben erre csak az összeg furcsa előjeléből következtethetnénk. (Ez az ellentétes előjel egyébként az egész számok belső, úgynevezett kettes komplemens kódú ábrázolási módjából adódik.)

Megpróbálhatjuk kijavítani az előbbi hibát úgy is, hogy átrendezzük az egyenlőtlenséget elkerülendő szam és osszeg osszeadását:

hiba:=(osszeg>maxint-szam) or (osszeg<-maxint-szam);

Sajnos ez a megoldás sem jó, mert előfordulhatnak a következő esetek is:

Próbálkozhatnánk az egyenlőtlenségek más átrendezésével is, mindig maradna olyan eset, amikor valamelyik irányban átléphetjük a maxint vagy -maxint határt.
Vegyük észre, hogy az osszeg+szam műveletben csak akkor lehet baj, ha azonos előjelű, tehát két pozitív vagy két negatív értéket adunk össze.
A helyes megoldás:

program osszegzes;
var szam,osszeg:integer;
    hiba:boolean;
begin
  osszeg:=0;
  repeat
    read(szam);
    if szam>0 then hiba:=osszeg>maxint-szam
    else hiba:=osszeg<-maxint-szam;
    if not hiba then osszeg:=osszeg+szam
  until (szam=0) or hiba;
  if hiba then writeln('az osszeg tul nagy!')
  else write('az osszeg: ',osszeg)
end.

Az előbbi problémának egyébként van egy másik megoldása is. Mivel általában a valós számok megengedett értéktartománya és ábrázolási pontossága is jóval nagyobb, mint az egészeké, az összegzést végezhetjük valós változóban is. A maxint-tel való összehasonlításkor, mivel a relációs jel egyik oldalán most már valós típusú összeg van, a gép először maxint-et is valóssá alakítja, majd az így kapott két valós értéket hasonlítja össze, tehát ekkor sem lehet baj.

2.8.1. Feladat:
Alakítsa át úgy az összegző programot úgy, hogy a hibaüzenetből az is kiderüljön, pozitív, vagy negatív irányban léptük-e túl a megengedett határt!

2.9. Egyenlet numerikus megoldása

A műszaki életben gyakran van szükség olyan érték kiszámítására, amelyet nem lehet, vagy nem érdemes teljes pontossággal meghatározni, de egy megfelelően pontos közelítő érték kiszámítása elegendő.
Az ilyen feladatokat úgynevezett numerikus módszerrel oldjuk meg. Tipikus numerikus probléma egy kezelhetetlen egyenlet megoldásának megkeresése, vagy egy hagyományos módszerekkel integrálhatatlan függvény határozott integráljának meghatározása. Ebben az alfejezetben az egyenletmegoldás, a következőben pedig a határozott integrál kiszámításának módszereivel foglalkozunk.

2.9.1. Példa:
Számítsuk ki legalább egymilliomod pontossággal, milyen x érték lesz az

e^x + 5x =5

egyenlet megoldása!
Ez az egyenlet azért kezelhetetlen, mert nem lehet átrendezéssel, azonos átalakításokkal olyan alakra hozni, hogy az egyenlőségjel egyik oldalán egyedül x, a másik oldalon pedig x-et nem tartalmazó kifejezés legyen.

Megoldás:
A könnyebb kezelhetőség kedvéért először rendezzük át az egyenletet úgy, hogy minden a bal oldalra kerüljön, vagyis a jobb oldalon 0 legyen:

e^x + 5x -5 = 0

Kézi számolással például úgy tudnánk az egyenlet megoldását valamilyen pontossággal meghatároz! hogy először találomra behelyettesítenénk néhány x értéket az egyenletbe, és figyelnénk, hogy mennyin teljesül, illetve nem teljesül az egyenlőség, milyen az eltérés jellege:

x e^x + 5x -5 = 0
5 168.41
-2 -1.48
1.5 6.98
1 2.72
0.5 -0.85
0.7 0.51

Ez a táblázat már sejteti a módszerünket: ha x=5-nél az egyenlet bal oldalán álló kifejezés értéke pozitív, x=-2-nél pedig negatív, és tudjuk, hogy a függvény folytonos, azaz nincs szakadása e két x érték között (és ez most teljesül), akkor azt is tudjuk, hogy valahol ebben a tartományban kell az egyenlet (legalább egy) megoldásának lennie. A következő behelyettesített értéket ezért valahol 5 és -2 között választjuk, például középen, és figyeljük az egyenlet bal oldalának értékét. Ha ez negatív lett volna, akkor a keresést az 1.5 ... 5 tartományban folytattuk volna, de pozitív lett, tehát a megoldásnak valahol -2 és 1.5 között kell lennie. A további tippeket is hasonlóan választottuk, bár nem mindig a lehetséges intervallum közepére.
Látható, hogy egyre kisebb lett a bal oldal értéke, és azt is sejtjük, hogy ha elég sok így megválasztott értéket próbálunk ki, akkor előbb-utóbb elérjük, hogy intervallumunk, amelyen belül az egyenlet megoldásának lennie kell, már rövidebb, mint 1e-6.
Ekkor akár az intervallum alsó, akár felső határát megoldásnak tekinthetjük, hiszen ha az alsó és felső határ közelebb van egymáshoz, mint a megengedett hibahatár, akkor a kettő között található megoldás sem lehet ennél távolabb.

Az egyenlet megoldását szokás az egyenlet gyökének is nevezni. Az egyes gyökkereső módszerek abban különböznek egymástól, hogy hogyan választjuk meg az előző tippek és az így kapott függvényértékek alapján a következő tippet.
Könnyíti az egyes módszerek bemutatását, ha a nullára rendezett egyenlet bal oldalát úgy tekintjük, mint x függvényét, és így az egyenlet megoldása egy függvény nullahelyének megkeresését jelenti. Az előbbi egyenletnél például:

f(x) = e^x + 5x - 5

és az f(x)=0 egyenlet gyökét, vagyis azt az x értéket keressük, ahol f(x)=0.

Intervallumfelezéses módszer
Egy f(x)=0 egyenlet gyökét keressük. Módszerünk csak akkor működik, ha valahonnan tudjuk, hogy az a...b intervallumon a függvényünk folytonos és egy gyöke van, tehát az intervallum végpontjaiban ellentétes előjelű.
Először tegyük fel, hogy a függvény monoton nő, azaz f(a)<0 és f(b)>0. Ekkor az intervallumfelezéses módszer szerint a megoldást az intervallum közepén keressük, vagyis a tippünk a következő:

x=(a+b)/2

Ha itt f(x)<0, akkor x lesz az intervallum új alsó határa, ha f(x)>0, akkor pedig az új felső határa.
A határok ilyen módosítását, vagyis az intervallum szűkítését addig kell folytatni, amíg az a...b intervallum hossza kisebbé nem válik, mint a megengedett hibahatár.

A vázolt gondolatmenet alapján a program könnyen megírható:

program intervallumfelezes1;
var a,b,x,hibahatar:real;
begin
  a:=-2;b:=5;hibahatar:=1E-6;
  repeat
    x:=(a+b)/2;
      if exp(x)+5*x-5<0 then a:=x
      else b:=x
    until b-a<hibahatar;
  writeln('Az egyenlet megoldasa:  x=',x:10:6)
end.

A programot Turbo Pascalban futtatva, eredményül a következőt kapjuk: x = 0.625984

Az eredmény kiírásához nem véletlenül választottuk a 6 tizedesjegy pontosságot. Ennél pontosabb eredményt a valós számok ábrázolási pontatlansága miatt nem kaphatunk. Ha például a kiírási formátum :14:10 lenne, akkor becsapnánk az eredmény felhasználóját, mert a kiírás pontosságából arra lehetne következtetni, hogy az eredmény 10 tizedesig pontos. Ha pedig :8:4 lenne a formátumelőírás, akkor magát a számítást nem lenne érdemes hat tizedesjegyig végezni.
Általánosítva úgy fogalmazhatunk, hogy a kiírás pontosságát a számítás pontosságához kell igazítanunk.

Itt jegyezzük meg, hogy a számítás hibahatárát nem érdemes olyan alacsonyra választani, hogy összemérhető legyen a gép számolási pontosságával. (Pl. HiSoft-Pascalban a programot elegendő hibahatar:=1E-5 megadásával futtatni.)

Érdemes megvizsgálni, hogy milyen gyorsan adja az előbbi módszer az egyenlet adott pontosságú megoldását.

Mivel minden lépés az előző felére csökkenti az intervallum hosszát, k lépés után az intervallum h hossza a kezdeti b-a értékről

h=(b-a)/2^k

értékre csökken. A legvégén h<hibahatár, azaz

h=(b-a)/2^k < hibahatár

ebből

itt log2 kettes alapú logaritmust jelent.
Az egyenlőtlenség jobb oldala nem feltétlenül egész mennyiség, így k tényleges értékét úgy kapjuk meg, hogy felkerekítünk a legközelebbi egészre.
Ebben a példában 23 lépés szükséges a megoldáshoz. (A HiSoft-Pascal számítási pontosságához 20 is elegendő.)

A program ennél az egyenletnél jól működik, de ha más egyenletek megoldására is használni akarjuk, akkor ki kell egészítenünk.
Ha f(a)>0 és f(b)<0, akkor

Ha pedig általánosan használhatóra szeretnénk megírni a programot, tehát hogy akkor is jól működjön, ha a függvény pozitív, és akkor is, ha negatív irányba lépi át az x tengelyt, akkor szintén van több lehetőségünk. Ehhez a repeat ciklusba lépés előtt meg kell határoznunk a függvény előjelét az intervallum valamelyik végén, például a-nál (fa valós változó):

fa:=exp(a)+5*a-5;

if fa<0 then { novekvo }
  if exp(x)+5*x-5<0 then a:=x
  else b:=x
else { csokkeno }
  if exp(x)+5*x-5>0 then a:=x
  else b:=x;

Találhatunk ennél lényegesen egyszerűbb megoldást is:

Az if utasítás ezzel így módosul:

if novekvo then
{ növekvő } if ...
else { csökkenő } if ...

A belső if utasításokban a then és az else rész ugyanolyan, ezért azt sejtjük, hogy ezek biztosan összevonhatók egyetlen if utasításba. Erre az ad lehetőséget, hogy logikai mennyiségeket is összehasonlíthatunk az =, <> stb. relációs műveletek segítségével. Figyeljük meg például a

novekvo=(exp(x)+5*x-5<0)

kifejezés értékét, ahol novekvo az előző megoldás szerinti logikai változó:

novekvo (exp ...) novekvo=(exp...)
igaz igaz igaz
igaz hamis hamis
hamis igaz hamis
hamis hamis igaz

Azt láthatjuk, hogy csökkenő függvény esetén ennek a logikai kifejezésnek az értéke pont ellentéte a függvényérték<0 vizsgálat eredményének, így az if feltételének elegendő ezt a kibővített kifejezést beírni.
Az ennek megfelelően átalakított program:

program intervallumfelezes2;
var a,b,x,hibahatar:real;
    novekvo:boolean;
begin
  a:=-2;b:=5;hibahatar:=1E-6;
  novekvo:=exp(a)+5*a-5<0;
  repeat
    x:=(a+b)/2;
    if novekvo=(exp(x)+5*x-5<0) then a:=x
    else b:=x
  until b-a<hibahatar;
  writeln('Az egyenlet megoldasa:  x=',x:10:6)
end.

2.9.1. Feladat:
Mi történik az intervallumfelezéses módszer használata esetén, ha a kijelölt intervallumban a függvénynek több gyöke, azaz megoldása van?
És ha egyetlen megoldása sincs?

2.9.2. Feladat:
Alakítsa át az előző programot úgy, hogy adjon megfelelő hibaüzenetet, ha nincs megoldása az egyenletnek az adott tartományban! Feltételezzük, hogy az egyenletből származtatott függvény folytonos.

2.9.3. Feladat:
Írhatjuk-e a programunkban az if utasítás feltételét a következő módon?
if novekvo=exp(x)+5*x-5<0 then ...

Húrmódszer
Továbbra is az előbbi feladatban szereplő egyenletet akarjuk megoldani, de most más módszerrel választjuk az egymást követő tippeket.
Amikor először helyettesítettünk néhány x értéket az egyenletbe, és azt kaptuk, hogy x=5-nél a függvény értéke kb. 168, x=-2-nél pedig azt, hogy -1.48, akkor már sejthettük, hogy a megoldás közelebb lesz a -2-höz, mint 5-höz. Az úgynevezett húrmódszer azt feltételezi, hogy függvényünk nagyjából lineáris, azaz alakja nem nagyon tér el az egyenestől. Hamarabb kapunk tehát megoldást, ha nem az intervallum közepén keressük, hanem ott, ahol az az egyenes metszi az x tengelyt, amelyik az intervallum két végén kapott függvényértékeket összeköti.

A metszéspont x-értékét a hasonló háromszögek segítségével tudjuk kiszámítani:

ebből:

Az intervallum új határait a függvény x-beli előjele alapján ugyanúgy változtatjuk meg, mint az intervallumfelezéses módszernél: ha f(x)<0, akkor x az új alsó, egyébként pedig az új felső határ lesz.
Az ennek megfelelő program mindjárt általánosabb, tehát akár csökkenő, akár növekvő függvény kezelésére alkalmas:

program hurmodszer;
var a,fa,b,fb,x,fx,regix,hiba:real;
    novekvo:boolean;
begin
  a:=-2;b:=5; { also-, felso hatar }
  fa:=exp(a)+5*a-5; { fuggvenyertek a-nal }
  fb:=exp(b)+5*b-5; { fuggvenyertek b-nel }
  hiba:=1E-6;       { hibahatar }
  novekvo:=fa<0;    { novekvo a fuggveny? }
  x:=a;  { kezdeti ertek }
  repeat
    regix:=x; { elozo ertek }
    x:=a-fa*(b-a)/(fb-fa);  { uj tipp }
    fx:=exp(x)+5*x-5;
    if (fx<0)=novekvo then begin { also hatar valtozik }
      a:=x;fa:=fx
    end
    else begin  { felso hatar valtozik }
      b:=x;fb:=fx
    end
  until abs(regix-x)<hiba;
  writeln('Az egyenlet megoldasa:  x=',x:10:6)
end.

Mivel a függvénynek az intervallum alsó és felső szélén felvett értékeire kétszer is szükségünk van a ciklus egyetlen végrehajtása során, nem érdemes ezeket ismételten kiszámolni. Félretettük őket az fa és fb változóba, és a határ megváltoztatásakor az x helyen kiszámított értéket is átmásoljuk a megfelelőbe közülük.
A megoldás érdekessége még, hogy nem az intervallum hosszát vizsgáljuk a ciklus leállási feltételében, hanem azt, hogy mennyire különbözik az új tipp az előzőtől. Ez azért van így, mert egyes esetekben, mint például most is, mindig csak az alsó határ, azaz a változik, b mindig 5 marad, azaz az intervallum hossza sohasem lesz a hibahatár alatt. Így, ha a leállási feltétel abs(b-a)<hiba lenne, végtelen ciklusba kerülnénk.

A végtelen ciklus olyan utasítássorozatot jelent, amely újra és újra végrehajtódik, és ez az ismétlődés so-hasem fejeződik be. Ilyenkor az egyik befejezési lehetőség, hogy a programot a gép megfelelő parancsával leállítjuk ("lelőjük"), ez a Turbo Pascal hasznalata esetén például a Ctrl és a C billentyű együttes leütésével lehetséges (ha a program elején megadtuk a {$u+ } opciót). Az is lehetséges, hogy egy rendszerprogram figyeli, mennyi időt használt el a programunk, és ha ez túllépi a megengedett értéket, akkor leállítja. A végtelen ciklus elég gyakran előforduló programozói hiba, érdemes a program tervezésekor és kipróbálásakor külön felkészülni rá.
A gyökkeresési feladatnál például, ha nem vagyunk biztosak abban, hogy a program megtalálja a keresett gyököt, vagy előfordulhat, hogy csak nagyon sok lépés után, akkor érdemes a ciklusok lefutását egy változóban számlálni, és a ciklust akkor is leállítani, ha ez a számláló már túl nagy, és fennáll a veszélye, hogy végtelen ciklusba kerültünk.

A program 67 lépés után találja meg az egyenlet megoldását. Ez nem éppen kedvező, lényegesen rosszabb, mint az intervallumfelezéses módszernél kapott érték. Annak meghatározása, hogy a húrmódszer esetén milyen gyorsan kapunk megfelelően jó közelítést a keresett gyökre, azaz hogy a kö?zelítés milyen gyorsan konvergál a megoldáshoz, sokkal nehezebb, mint az intervallumfelezéses módszer esetén, itt nem is foglalkozunk vele. Erről, és a numerikus módszerekről általában, az olvasó figyelmébe ajánlom a [8] irodalmat. Az ebben található becslés szerint az x0 gyöknek és az xk k-dik közelítésnek a távolsága a következő módon becsülhető:

ahol

M a függvény második deriváltja abszolút értékének maximuma,
m pedig a függvény első deriváltja abszolút értékének minimuma az a... b tartományban, m<>0.

Lassú konvergenciája miatt a húrmódszert ilyen formában ritkán alkalmazzuk.

Érintőmódszer (Newton-Raphson)
Ez a módszer is azon a feltételezésen alapul, hogy a függvény elég lineáris a nullahely környezetében. Most nem a függvény görbéjének két, ellentétes előjelű pontját összekötő egyenes mentén keressük a megoldást, hanem mindig ott, ahol a megfelelő tippnél a függvény görbéjéhez húzott érintő metszi az x tengelyt.

Az érintő meredekségét meghatározhatjuk az f(x) függvény f '(x) deriváltfüggvénye és az ábra szerinti háromszög alapján is:

ebből

Ennél a módszernél tehát nem két előző x érték által meghatározott tartományon belül keressük a megoldást, hanem mindig csak az előző egyetlen x érték alapján. Itt azonban szükségünk van a függvény deriváltjára is. A keresést itt is akkor fejezzük be, ha az utolsó két x érték már csak kevéssé tér el egymástól.
Az eddigi példáinkban szereplő függvény deriváltja:

f'(x) = e^x + 5

A program:

program erintomodszer;
var x,ujx,hiba:real;
begin
  ujx:=5;   { kezdeti x ertek }
  hiba:=1E-6;
  repeat
    x:=ujx; { elozo x ertek }
    ujx:=x-(exp(x)+5*x-5)/(exp(x)+5) { uj tipp }
  until abs(ujx-x)<hiba;
  writeln('Az egyenlet megoldasa: x=',x:10:6)
end.

Ennek a módszernek nemcsak egyszerűsége, de gyorsasága is vonzó, hiszen (bizonyos további feltételei teljesülése esetén: [8]) a húrmódszernél bevezetett jelölésekkel:

ami q<1 esetén nagyon gyors konvergenciát jelent, a mi példánkban például x=5-nél indulva 8 lépés is elég.

Szelőmódszer
Az érintőmódszert kellemes tulajdonságai ellenére nem használhatjuk akkor, ha valamilyen oknál fogva nem tudjuk meghatározni a függvény deriváltját. Például azért, mert maga a függvény nem képlet formájában áll rendelkezésünkre, hanem egy további, esetleg meglehetősen összetett programrész számítja ki.
Ilyenkor az érintőt több-kevesebb pontossággal helyettesíthetjük egy szelővel is, azaz a függvény két pontjára fektetett egyenes és az x tengely metszéspontját tekintjük új x-tippnek, a következő lépésben pedig az ebben és az előző x pontban kapott függvénypontokat kötjük össze stb.

Az új x érték ismét hasonló háromszögek segítségéve! határozható meg:

amiből

tehát ugyanazt kaptuk, mint a húrmódszer esetén. Most azonban nem kötöttük ki, hogy az x0, illetve x1 értékeknél a függvény előjele különböző legyen, és mindig az utoljára kapott két pontra fektetjük az új x értéket megadó egyenest.

program szelomodszer;
var x1,fx1,x0,fx0,x,hiba:real;
begin
  x1:=4;x0:=5; { indulási pontok }
  fx1:=exp(x1)+5*x1-5; { fuggvenyertekek }
  fx0:=exp(x0)+5*x0-5;
  hiba:=1e-6;
  repeat
    x:=x1-fx1*(x0-x1)/(fx0-fx1);
    x0:=x1;fx0:=fx1;
    x1:=x;fx1:=exp(x1)+5*x1-5
  until abs(x1-x0)<hiba;
  writeln('Az egyenlet megoldasa:  x=',x:10:6);
end.

Programunknak 9 lépésre van szüksége a megoldáshoz.
A k-adik lépésben kapott xk közelítő érték és k0 gyök távolságát a következő képlettel határozhatjuk meg [8]:

ahol

és F(k+1) a k+1-edik Fibonacci-szám:

F(0)=0, F(1)=1, F(2)=1, ..., F(k+1)=F(k)+F(k-1)

Ha összehasonlítjuk a húr-, az érintő- és a szelőmódszert, akkor azt látjuk, hogy a konvergencia gyorsaságát a mindháromnál hasonló jelentésű q mennyiség kitevője határozza meg:

Lépésszám 0 1 2 3 4 5 6 7 8 9 10
Húrmódszer 1 1 2 3 4 5 6 7 8 9 10
Érintőm. 1 2 4 8 16 32 64 128 256 512 1024
Szelőm. 1 1 2 3 5 8 13 21 34 55 89

A táblázatból az derül ki, hogy az érintőmódszer a leghatékonyabb. Ha azonban figyelembe vesszük azt is, hogy ennél minden lépésben nemcsak a függvény, hanem a derivált értékét is ki kell számítani, ami körülbelül ugyanannyi műveletet igényel, mint magának a függvénynek a kiszámítása, akkor az érintő-módszer minden lépését a másik két módszer minden második lépéséhez kell hasonlítani. Ekkor már a szelőmódszer tűnik hatásosabbnak:

Lépésszám 0 1 2 3 4 5 6 7 8 9 10
Húrmódszer 1 1 2 3 4 5 6 7 8 9 10
Érintőm. 1   2   4   8   16   32
Szelőm. 1 1 2 3 5 8 13 21 34 55 89

Ha a függvényt és deriváltját megadó képlet nagyon hasonló - ahogyan mintapéldánknál is érintőmódszer esetén előnyösebb, ha az új x érték meghatározásakor a derivált értékét nem közvetlenül, hanem a függvényértékből számítjuk ki:

fx:=exp(x)+5*x-5;
ujx:=x-fx/(fx-5*x+10); {új tipp}

Az intervallumfelezéses módszer előnye a többivel szemben, hogy olyan függvények esetén is használható, amikor a többi módszer az itt nem részletezett egyéb megkötések miatt nem, vagy csak kis hatékonysággal működik.
Lehet keverten is használni az előbbi módszereket. Az első néhány lépésben például intervallumfelezéssel eljutunk egy olyan kis tartományig, ahol a függvény már nagyon jó közelítéssel lineáris. Innentől pedig az előbbi három módszer valamelyikét, célszerűen az érintő- vagy a szelőmódszert használjuk:

program felezo_majd_szelomodszer;
var x1,x0,fx1,fx0,x,hibahatar:real;
begin
  { --- intervallumfelezes --- }
  x0:=-2;x1:=5; { indulasi pontok }
  hibahatar:=1;
  repeat
    x:=(x0+x1)/2;
    if exp(x)+5*x-5<0 then x0:=x
    else x1:=x
  until x1-x0<hibahatar;

  { --- szelomodszer --- }
  fx1:=exp(x1)+5*x1-5;
  fx0:=exp(x0)+5*x0-5;
  hibahatar:=1E-6;
  repeat
    x:=x1-fx1*(x0-x1)/(fx0-fx1);
    x0:=x1;fx0:=fx1;
    x1:=x;fx1:=exp(x1)+5*x1-5
  until abs(x1-x0)<hibahatar;
  writeln('Az egyenlet megoldasa:  x=',x:10:6)
end.

Ez már 7 lépésben adja az eredményt, tehát az összes tárgyalt módszer közül a leggyorsabb, a 7-ből 3 az intervallumfelezésre, 4 a szelőmódszerre kellett.

2.10. Határozott integrál numerikus meghatározása, a for utasítás

2.10.1. Példa:
Számítsuk ki 0.1% pontossággal az

f(x) = sin(x)/x

függvény határozott integrálját a 0.1 ... 10 tartományban!
A feladat nehézségét az okozza, hogy ennek a függvénynek nincsen zárt alakban megadható primitív függvénye, azaz olyan függvény, amelynek sin(x) /x lenne a deriváltfüggvénye.

Megoldás:
Azt a tulajdonságot fogjuk kihasználni, hogy egy egyváltozós függvény adott a...b határok közötti határozott integrálját úgy is felfoghatjuk, mint a függvény alatti területet, vagyis a függvény görbéje és az x tengely közötti terület nagyságát az adott tartományban (ld. ábra).

A sin(x)/x függvény integrálja 0.1 és 10 között

Ezt a függvény alatti területet úgy tudjuk közelítőleg meghatározni, hogy az integrálás a...b tartományát felosztjuk n darab

h = (b-a)/n

hosszúságú szakaszra, egy-egy ilyen kis szakaszon pedig az f(x) függvény alatti területet valamilyen egyszerűbb mértani alakzat területével helyettesítjük. Ez a helyettesítő alakzat lehet téglalap, trapéz, vagy olyan alakzat, amelynek a teteje másodfokú parabola, az oldalai és az x tengellyel érintkező oldala pedig egyenes. Az egyes numerikus integrálási módszereket tehát elsősorban az különbözteti meg, hogy milyen alakzattal közelítjük az integrálandó függvényt az egyes h hosszúságú szeletekben.
Azt is sejthetjük, hogy annál pontosabb lesz a közelítés, minél jobban illeszkedik az alakzat a függvényhez, illetve minél nagyobb n értéket választunk.

Téglalapmódszer
Ez a legegyszerűbb, és már most tegyük hozzá, a legkevésbé hatékony módszer.
Egyetlen h hosszúságú tartományban a közelítő téglalap területét a

t = f(x)*h

kifejezés adja. Ha pedig a teljes a...b tartományra összeadjuk ezeket a t értékeket, akkor a teljes Tn területet h kiemelése után így fejezhetjük ki:

Tn = h*(f(a) + f(a+h) + f(a+2h) + ... + f(a+(n-1)h))

Itt éppen n értéket kell összegezni: az integrálandó függvény értékét az a, a+h, a+2h, a+3h, ..., a+(n-1)*h helyeken, majd az így kapott eredményt meg kell szorozni h-val.

Az ennek megfelelő program:

program teglalap_modszer;
var x,h,t,a,b:real;
    n,k:integer;
begin
  write('n= ');read(n);
  a:=0.1;b:=10; { integral hatarok }
  h:=(b-a)/n;   { lepeshossz }
  t:=0;x:=a;k:=0;  { kezdeti ertekek }
  repeat
    t:=t+sin(x)/x; { osszegzes }
    x:=x+h;k:=k+1  { x novelese, ciklusszamlalas }
  until k=n;
  t:=t*h;
  writeln('sin(x)/x integralja ',a:10:5,
          ' es ',b:10:5,' kozott: ',t:10:5)
end.

Turbo Pascalban read(n) helyett readln(n) írandó.
Ez a program az n értéket, vagyis azt, hogy az a...b intervallumot hány szakaszra osztjuk fel, bemeneti adatként kapja meg, tehát ezt a program felhasználójának kell megadnia. Ha kipróbáljuk a programot, és különböző n értékekkel futtatjuk, akkor a következő eredményeket, illetve futási időket kapjuk (itt is, mint minden további példánál, Turbo Pascal 3.0-át használtunk IBM-PC/XT - 8087 matematikai segédproceszszor nélkül - illetve 6MHz-s Enterprise gépeken):

n
integrál
Idő (másodperc)
IBM-PC/XT
Enterprise
100
1.61048
1.76
2.4
1000
1.56361
15.27
22.9
10000
1.55892
151.15
228.8

Azt tapasztaljuk, hogy ha az a...b szakaszt csak 100 részre osztjuk fel, akkor nagyon pontatlan becslést kapunk az integrál értékére. Ha viszont a pontszámot 1000-ről 10000-re növeljük, akkor a kapott két közelítő érték között már 1% eltérés sincs. Vajon meddig érdemes növelni a pontszámot? Ha a pontszám nő, akkor ezzel arányosan nő a program futási ideje, de vajon milyen arányban lesz a megoldás pontosabb?
Megmutatható, hogy téglalapmódszernél a Tn közelítő összeg és a tényleges I integrálérték különbsége a következőképpen becsülhető [8]:

tehát ha kétszeresére növeljük a pontszámot, akkor a hiba a felére csökken. Ebből az is következik, hogy ha további tízszeres pontosságnövekedést akarunk elérni - ami az eredeti célkitűzésünk szerinti 0.1% pontosságot biztosítaná -, akkor körülbelül százezer pontra kellene felosztani a 0.1 ... 10 tartományt. Ez nemcsak azért kellemetlen, mert ekkor a program körülbelül 1500 másodpercig, azaz majdnem fél óráig futna, hanem azért is, mert a legtöbb személyi számítógépen, így az általunk használton is maxint=32767, tehát n és k nem is lenne tárolható egész számként, legfeljebb valósként, és ez tovább növelné a program futási idejét. A gép ugyanis lényegesen gyorsabban tud egészekkel számolni.
A pontszám növelésekor érdekes módon egy további kellemetlen jelenséget is tapasztalunk: az integrálközelítő összeg hibája bizonyos - elég magas - pontszám felett már nem csökken, hanem nő. Ez azért van, mert az eredményt ekkor már nagyon sok számolás eredményeként kapjuk, és mivel a valós számok használatakor minden egyes számítás behoz egy kis hibát, ezek a hibák - bár nem egyszerűen összeadódnak, mégis - halmozottan jelentkeznek. Az integrálási lépésköz finomítása tehát csak bizonyos szintig javíthatja a pontosságot. E probléma részletesebb tárgyalására itt nem térhetünk ki, de a numerikus módszerek szakirodalma részletesen foglalkozik vele.
Más, jobb integrálközelítő módszert kell tehát használnunk.

Trapézmódszer
Az integrálandó függvény alatti területet minden egyes h hosszúságú szakaszon olyan trapéz területével közelítjük, amely a szakasz végein illeszkedik a függvényhez.

Egy ilyen trapéz területe:

A teljes Tn közelítő érték

Azaz a két szélső pontban a függvényértékek felét, minden belső pontban pedig a függvényértékeket kell összegezni, majd ezt szorozni h-val.

A program:

program trapezmodszer;
var x,h,t,a,b:real;
    n,k:integer;
begin
  write('n= ');read(n);  { pontszam beolvasasa }
  a:=0.1;b:=10; { integralasi hatarok }
  h:=(b-a)/n;   { lepeshossz }
  t:=(sin(a)/a+sin(b)/b)/2;  { kezdeti ertek }
  x:=a+h;k:=1;
  repeat
    t:=t+sin(x)/x; { osszegzes }
    x:=x+h;k:=k+1  { x novelese, szamlalas }
  until k=n;
  writeln('sin(x)/x integralja ',a:10:5,
          ' es ',b:10:5,' kozott: ',t*h:10:5)
end.

Turbo Pascalban read(n) helyett readln(n) írandó.
A programot különböző n értékekkel lefuttatva a következőt kapjuk:

n
integrál
Idő (másodperc)
IBM-PC/XT
Enterprise
100
1.55837
1.75
2.3
200
1.55839
3.24
4.6
500
1.55840
7.75
11.5
1000
1.55840
15.32
22.9

Azt tapasztaljuk, hogy n értékét 500-ról 1000-re növelve már az eredmény hatodik számjegye sem változik, tehát felesleges a szakasz felosztását tovább finomítani.
A trapézmódszer hibája a következő képlettel becsülhető:

ahol f'(b), illetve f '(a) a függvény deriváltjai az adott helyen [8].

Az előbbi képletből leolvasható, hogy az n pontszám duplázásakor a közelítés pontossága négyszeresére nő. Trapézmódszerrel tehát lényegesen gyorsabban megkaphatjuk a kívánt pontosságú integrálértéket, mint téglalapmódszerrel, pedig a program bonyolultsága és futási ideje gyakorlatilag nem is nőtt. A téglalapmódszert ezért a gyakorlatban sohasem használjuk.

n értékét eddig bemeneti adatként kezeltük, de jobb lenne, ha n megfelelő növelését és az eredmény pontosságának vizsgálatát maga a program végezné el.
Ezt úgy oldhatjuk meg például, hogy a program egy további, az egész közelítést magába foglaló ciklusban duplázza a pontszámot, és minden ciklus végén megvizsgálja, mennyit változott az integrálközelítő összeg. Ha a különbség elég kicsi, nem kell tovább folytatni a pontszám növelését. A gondolatmenet vázlata:

kezdeti pontszám beállítása
ismétlendő:
   integrálközelítő összeg meghatározása
   pontszám duplázása
ismétlés vége, ha a két utolsó közelítő összeg elég közel van egymáshoz

A módszer jogosságát az biztosítja, hogy a legtöbb gyakorlati esetben a pontszám duplázásával kapott két közelítő összeg különbsége nagyobb, mint az utolsó közelítő összeg és az elméleti integrálérték közötti különbség. Konkrétan a trapézmódszernél [8]:

A ciklus első végrehajtásakor még nincs előző közelítő érték, vegyük ezt az első esetben nullának.
Mivel a 2.10.1 példa szerint az a feladatunk, hogy az integrál értéke 0.1% pontosságú legyen, a hibát magához a kapott közelítő értékhez kell hasonlítanunk. Ez az úgynevezett relatív hiba vizsgálatát jelenti: a két közelítő érték különbségének és az utolsó közelítő értéknek a hányadosát hasonlítjuk a megadott hibahatárhoz.
Az utóbbi megfontolások szerint pontosított programvázlat:

kezdeti pontszám beállítása
előző közelítő érték legyen nulla
ismétlendő:
   integrálközelítő érték meghatározása
   pontszám duplázása
ismétlés vége, ha a két utolsó közelítő érték relatív eltérése kisebb, mint 0.1%

Az ennek megfelelő program:

program trapezmodszer2;
var x,h,t,et,a,b,relativhiba:real;
    n,k:integer;
begin
  relativhiba:=0.001;
  n:=20;t:=0;   { pontszam kezdeti erteke }
  a:=0.1;b:=10; { integralasi hatarok }
  repeat   { finomitas ciklusa }
    et:=t; { elozo kozelito ertek }
    h:=(b-a)/n;  { lepeshossz }
    t:=(sin(a)/a+sin(b)/b)/2;  { kezdeti ertek }
    x:=a+h;k:=1;
    repeat
      t:=t+sin(x)/x;  { osszegzes }
      x:=x+h;k:=k+1   { x novelese, szamlalas }
    until k=n;
    t:=t*h;
    n:=n+n  { pontszam duplazasa }
  until abs((t-et)/t)<3*relativhiba;
  writeln('sin(x)/x integralja ',a:10:5,
          ' es ',b:10:5,' kozott: ',t:10:5)
end.

A program már n=40-nél befejeződik, a kiírt eredmény: T=1.55817, a két utolsó közelítő összeg közötti relatív hiba 0.0446%, a program pedig mindössze 1.37 másodpercet használ el.

Mielőtt azt vizsgálnánk, hogy milyen módszerekkel lehet tovább növelni az ilyen programok hatékonyságát érdemes megismerkedni egy újabb Pascal utasítással.
Az előző - és sok egyéb, például az első fejezetben látott - programban előfordult, hogy olyan ciklust használtunk, amelyben egy egész típusú ciklusváltozó értéke egy előre meghatározott kezdeti értéktől egy szintén előre ismert végértékig egyesével növekedett. Nagyon gyakori, hogy ilyen vagy ehhez hasonló ciklust kell használnunk, azaz olyat, amelynél nem a ciklusmag végrehajtása során derül ki, hogy tovább kell-e folytatni az ismétlést, hanem az ismétlések számát már a ciklusba való belépéskor ismerjük. Gyakorisága miatt erre a feladatra külön ciklusutasítás létezik, amely elvégzi a ciklusváltozó kezdeti értékének beállítását, a ciklusváltozó növelését, illetve csökkentését, és a ciklus leállási feltételének vizsgálatát is. Ez a for utasítás.


A for utasítás

1. Alakja:

2. A változóazonosító a for utasítás ciklusváltozóját adja meg; ennek az értéke vezérli a ciklus végrehajtását.
Csak sorszámozott típusú lehet (tehát integer, boolean, char, felsorolt típus vagy ezek intervalluma).
A ciklusváltozónak önálló változónak kell lennie, tehát nem lehet eleme sem tömbnek, sem rekordnak, sem file-nak, illetve nem lehet mutató segítségével megadott dinamikus változó sem.
A ciklusváltozónak lokálisnak kell lennie, azaz deklarációjának a for utasítást közvetlenül tartalmazó blokkban kell lennie.

3. A kifejezés1 a ciklusváltozó kezdeti értékét, a kifejezés2 a változó végértékét adja meg. Mindkét kifejezésnek a ciklusváltozóval kompatibilis típusúnak kell lennie, ha pedig a do után álló utasítás - amit ciklusmagnak is szoktunk nevezni - akár egyszer is végrehajtódik, értékadás-kompatíbilisnek. (a)

4. A ciklus lefutása után a ciklusváltozó értéke meghatározatlan, kivéve, ha goto utasítással léptünk ki a ciklusból. (b)

5. A ciklusváltozó értékét ciklusmagbeli utasításnak nem szabad megváltoztatnia. (c)

6. A for v:=e1 to e2 do mag ciklus végrehajtását a következő programrészlet definiálja:

begin
  atm1:=e1;atm2:=e1; { kezdeti ertek, vegertek }
  if atm1<=atm2 then begin { legalabb egyszer lefut }
    v:=atm1; { ciklusvaltozo kezdeti erteke }
    ... { ciklusmag vegrehajtasa }
    while v<>atm2 do begin
      v:=succ(v); { ciklusvaltozo novelese }
      ... { ciklusmag vegrehajtasa }
    end; { while }
  end; { if }
end

vagyis a v ciklusváltozó az e1 kezdeti értéktől az e2 végértékig nő, és minden egyes értéknél végrehajtódik a do után álló utasítás.
Hasonlóan a for v:=e1 downto e2 do mag ciklus végrehajtása:

begin
  atm1:=e1;atm2:=e1; { kezdeti ertek, vegertek }
  if atm1>=atm2 then begin { legalabb egyszer lefut }
    v:=atm1; { ciklusvaltozo kezdeti erteke }
    ... { ciklusmag vegrehajtasa }
    while v<>atm2 do begin
      v:=pred(v); { ciklusvaltozo csökkentese }
      ... { ciklusmag vegrehajtasa }
    end; { while }
  end; { if }
end

Ez abban különbözik az előzőtől, hogy a ciklusváltozó a kezdeti értéktől a végértékig nem növekszik, hanem csökken.

Az előbbiekben atm1 és atm2 két olyan, külön, csak erre a célra használt változót jelent, amely értékadás-kompatibilis e1-gyel, illetve e2-vel, és a program egyéb részeiben nem használjuk. (d)
A működési leírásból látszik, hogy az e1 és e2 kifejezés kiértékelése is csak egyszer, a ciklusba lépéskor történik meg, így e kifejezések összetevőit hiába változtatjuk meg a ciklusmagban, a ciklus lefutását ezzel nem tudjuk befolyásolni. (e)

Magyarázatok, megjegyzések:

(a) A 3. pontnak ez a megkötése tehát megengedi a következő esetet:

var g:1..10;
...
begin
  ...
  for g:=12 to -8 do c:=c+g;

mert a ciklusmag egyszer sem hajtódik végre. Ugyanezzel a g ciklusváltozóval hibás azonban a következő:

for g:=6 to 12 do ...

mert a végérték kilóg g megengedett tartományából.

(b) Nagyon nem szép megoldás a for ciklusból goto-val kilépni.
Ha valamilyen okból ez mégis kényelmesebbnek tűnne, akkor érdemes a ciklust while vagy repeat ciklussá alakítani, pl.

for g:=ettol to eddig do begin
  ...
  if a>b then goto 111;
  ...
end;

A goto helyett ajánlható:

g:=ettol;
repeat
  ...
    if a<=b then begin
    ...
    g:=g+1
  end
until (g>eddig) or (a>b);

Ha a kilépés feltételének kiértékelése összetett művelet, például azért, mert vagy a vagy b sok számítást igénylő függvény hívását jelenti, a leállási feltételt pl. egy vege nevű változóban tárolhatjuk:

g:=ettol;
repeat
  ...
  vege:=a>b;
  if not vege then begin
    ...
    g:=g+1
  end
until (g>eddig) or vege;

Ha az előbbi példában az is előfordulhat, hogy a for ciklus magja egyszer sem hajtódik végre, a repeat helyett while ciklust kell használnunk:

g:=ettol; vege:=false;
while (g<=eddig) and not vege do begin
  ...
  vege:=a>b;
  if not vege then begin
    ...
    g:=g+1
  end { if }
end; { while }

(c) Az 5. szabály értelmében tilos a ciklusváltozót a ciklusmagban megváltoztatni, ez a következő módokon történhetne meg:

Természetesen for ciklusok esetleg további for, while és repeat ciklusokkal is egymásba ágyazhatóak, de ügyelnünk kell arra, hogy egymásba ágyazott for ciklusok különböző ciklusváltozókat használjanak. Helyes például a következő szerkezet:

for g:=1 to 10 do begin
  ...
  for k:=10 downto -10 do begin
    ...
  end;
  ...
end;

Ezzel ellentétben hibás a következő megoldás, mivel a g külső ciklusváltozót megváltoztatja a belső ciklus. Ez még akkor is hibás, ha g utána visszakapja előző értékét:

for g:=1 to 10 do begin
  ...
  k:=g;
  for g:=10 downto -10 do begin
    ...
  end;
  g:=k;
end;

(d) Az atm1 és atm2 nevű változókat csak a for utasítás végrehajtásának magyarázatához használtuk. A program tényleges végrehajtása során ezek csupán ideiglenes memóriaterületek használatát jelentik, nem pedig tényleges, bármilyen néven nevezett változó használatát.

(e) Mivel a ciklusváltozó kezdeti és végértékének kiértékelése csak egyszer történik meg, a (b) pontban tárgyalt problémát nem oldhatjuk meg az alábbi módon sem:

veg:=eddig;
for g:=1 to veg do begin
  if a>b then veg:=0 { Hatastalan! }
  else begin
    ...
  end
end;


A for utasítás segítségével numerikus integrálást végző programjaink áttekinthetőbb formába írhatók. Például a legutóbbi (trapezmodszer2 nevű) program for utasítással:

program trapezmodszer2;   { for ciklussal }
var x,h,t,et,a,b,relativhiba:real;
    n,k:integer;
begin
  relativhiba:=0.001;
  n:=20;t:=0;   { pomtszam kezdeti erteke }
  a:=0.1;b:=10; { integralasi hatarok }
  repeat   { finomiotas ciklusa }
    et:=t; { elozo kozelito ertek }
    h:=(b-a)/n; { lepeshossz }
    t:=(sin(a)/a+sin(b)/b)/2;  { kezdeti ertek }
    x:=a+h;
    for k:=1 to n-1 do begin
      t:=t+sin(x)/x;  { osszegzes }
      x:=x+h { x novelese }
    end;
    t:=t*h;
    n:=n+n;  { pontszam duplazasa }
  until abs((t-et)/t)<3*relativhiba;
  writeln('sin(x)/x integralja ',a:10:5,
          ' es ',b:10:5,' kozott: ',t:10:5)
end.

Érdemes megfigyelni, hogy a pontszám duplázását végző repeat ciklus nem alakítható át for ciklussá, illetve, hogy hogyan adtuk meg a for ciklusváltozójának végértékét.
A program végrehajtási ideje 1.16 másodpercre csökkent, ez annak tulajdonítható, hogy a for utasítási a gép saját nyelvére hatékonyabban fordítható le, mint akár az ugyanilyen esetben használt repeat, vagy a while.

Ezzel megismertük a Pascal nyelv mindhárom ciklusutasítását.

Mikor melyik ciklusutasítást használjuk?
Felvetődhet az olvasóban a kérdés: mikor melyik ciklusutasítást kell vagy érdemes használni, valóban szükség van-e mindhárom ciklusutasításra?
Nos, elvileg akár a repeat és if utasítások együttes használatával, akár a while utasítással meg lehetne oldani az összes ciklusszervezési kérdést, de ez mégsem célszerű, mert erősen rontaná a program olvashatóságát.
A for utasításnál már láttunk példát arra, hogy azt hogyan lehet while ciklussal és persze további nyelvi elemekkel (if, utasításcsoport) helyettesíteni. Az Olvasóra bízom, hogy fogalmazza meg az összes többi lehetséges esetet is (for helyett repeat, repeat helyett while, while helyei repeat).
A programok írása közben azonban szinte automatikusan adódik, hogy mikor melyik ciklusutasítást használjuk. Általában:

Minden egyéb esetben - tehát ha nem ismerjük előre a végrehajtások szükséges számát, ha a ciklusváltozónk nem sorszámozott típusú, ha nem egyesével kell léptetni, vagy ha a ciklus végrehajtása közben alakul ki a végérték - while vagy repeat utasítást használunk.

Furcsának tűnhet, hogy az előbbi szempontok között nem szerepel, hogy igaz vagy hamis ciklusfeltétel esetén kell-e a ciklust folytatni, hiszen ebben is különbözik a while és a repeat utasítás. Ezt a különbséget - például a not művelettel - könnyen áthidalhatjuk.

Előfordulhat még olyan ciklus is, amelyben a mag közepén derül ki, kell-e tovább folytatni a ciklust. Nincs olyan ciklusutasításunk, amely ezt az esetet közvetlenül leírja, de könnyen helyettesíthetjük:

repeat
  bevezető tevékenység;
  folytatas:=ciklusfeltétel;
  if folytatas then begin
    további tevékenység;
  end;
until not folytatas;

Az előbbi eset mintájára sok további "szabálytalan" ciklusváltozat is elképzelhető. Ezek hasonló módon könnyen összerakhatók az eddig tárgyalt utasításokból.

2.10.1 Feladat
Készítsen programot az exp(x^2) függvény -2 és 2 közötti határozott integráljának meghatározására érintőmódszerrel!

Simpson-módszer
Ez az egyik legpontosabb és ezért leggyakrabban használt eljárás: a függvényt a h hosszúságú intervallumon olyan másodfokú görbével (parabolával) helyettesítjük, amely az intervallum két szélén és közepén is egyenlő a függvénnyel.

A parabola alatti terület egy ilyen szakaszon:

A teljes a...b szakaszra kapott összeg

ahol

g = h/2 = (b-a)/n

és n páros szám.

A Simpson-módszer esetén tehát a függvényértékeket az integrálási intervallum szélein egyszeres, közben pedig felváltva négyszeres, illetve kétszeres súlyozással kell figyelembe venni, az így kapott összeget pedig a lépésköz harmadával kell megszorozni.
A módszer előnye az, hogy már kis n értékek esetén is nagyon jó közelítést ad, n növelésével pedig a pontossága rohamosan növekszik, hibája ugyanis n negyedik hatványával fordítottan arányos:

ahol M a függvény negyedik deriváltjának maximuma az a...b tartományban.
A módszer pontosságát az n duplázásával kapott egymást követő közelítő értékekkel is kifejezhetjük:

ez szintén nagyon előnyös.
A Simpson-módszert felhasználó program a pontszám növelésével együtt:

program simpsonmodszer;
var x,g,t,et,a,b,relativhiba,suly:real;
    n,k:integer;
begin
  relativhiba:=0.001;
  n:=10; { pontszam kezdeti erteke }
  a:=0.1;b:=10; { integralasi hatarok }
  t:=0;  { t kezdeti erteke }
  repeat { finomitas ciklusa }
    et:=t; { elozo kozelito ertek }
    g:=(b-a)/n;   { lepeshossz }
    t:=sin(a)/a+sin(b)/b;  { kezdeti ertekek }
    x:=a+g;suly:=4;
    for k:=1 to n-1 do begin
      t:=t+suly*sin(x)/x;  { osszegzes }
      x:=x+g;
      suly:=6-suly  { valtas 4 es 2 kozott }
    end;
    t:=t*g/3;n:=n+n
  until abs((t-et)/t)<15*relativhiba;
  writeln('sin(x)/x integralja ',a:10:5,
          ' es ',b:10:5,' kozott ',t:10:5)
end.

A program már n=20-nál megfelelően pontos eredményt ad, így mindössze 0.71 másodpercre van szükség!

Amikor duplázzuk a pontszámot - például a trapézmódszernél akkor az új közelítő összeg meghatározásakor a számítás fele megtakarítható, hiszen a közelítő összeg minden második értékét az előző lépésben már kiszámítottuk. Ha például a pontszám duplázása után 0.1-től h=0.01 lépésben végezzük AZ összegzést, akkor előzőleg a 0.1, 0.12, 0.14 stb. helyeken már kiszámítottuk és összegeztük a függvényértékeket.

2.10.2 Feladat:
Alakítsa át az előzőm Simpson-módszerrel számító programot úgy, hogy felhasználja az előző összeg értékét is! Gondoljon arra, hogy a trapézmódszernél eddig a pontszám duplázása után az új közelítő összeg meghatározásakor ismét kiszámítottuk azokat a függvényértékeket is, amelyeket előzőleg már összegeztünk. Mérje meg az így kapott programok futási idejét! Mit tapasztal?

Tipikus hiba:
Gyakran előfordul, hogy for utasítást olyan ciklus vezérlésére alkalmazunk, amelyben a for ciklusváltozóját nem is használjuk. Ez nem hiba, hiszen például tipikusan ilyenek voltak a fenti integrálási módszerek is, amelyekben a k ciklusváltozó értékét a for magjában nem használtuk. Ilyenkor logikusnak tűnne, hogy a ciklust - például a legutóbbi programban - a következőképpen szervezzük meg:

for x:=a+h to b-h do begin
  t:=t+suly*sin(x)/x;
  suly:=6-suly
end;

Ez azonban hibás, mert a for utasítás ciklusváltozója nem lehet real típusú, csak olyan, amelynek van ord értéke, azaz sorszámozott típus.
Hibás azért is, mert x-et most nem 1-gyel, hanem h-val kellene növelni a ciklusmag minden lefutása után, ezt pedig nem írhatjuk elő a for utasításban. A for csak egyesével növeli, vagy downto esetén egyesével csökkenti a ciklusváltozót.

Az, hogy a for utasítás ciklusváltozója nem lehet real típusú, biztonságosabbá teszi a programjainkat, hiszen a valós ciklusváltozó értékének minden egyes növelésekor újabb és újabb kerekítési pontatlanság keletkezhet, és ez bizonytalanná teszi a ciklus leállási feltételének bekövetkezését: lehet, hogy a felhalmozódott hibák miatt egy vagy néhány lépéssel előbb, vagy éppen később állna le a for ciklus.

2.11. A karakter (char) típus, a case utasítás

2.11.1 Példa:
Készítsünk programot, amely kitalálja, hogy milyen 1 és 100 közötti egész számot gondolt a program használója!
A programot a következő módon lehessen használni: a gép kiír egy tippet, erre a felhasználó a -1,1 vagy 0 számok valamelyikével válaszol, annak megfelelően, hogy az általa gondolt szám a tippnél kisebb (-1), nagyobb (1) vagy azzal egyenlő (0). Ha eltalálta, akkor a játék befejeződik, ha nem, akkor a program a kapott válasznak megfelelően tovább tippel.
Például:

Tipp Válasz
50
-1
(kisebb)
25
1
(nagyobb)
37
1
(nagyobb)
43
1
(nagyobb)
46
-1
(kisebb)
44
1
(nagyobb)
45
0
(talált)

Megoldás:
A tippeket lehetne a programban valahogy véletlenszerűen is választani, ez azonban biztosan nem a leggyorsabb megoldás. Az előbbi példából is sejthető, hogy a tippet a még lehetséges számtartomány közepén érdemes megválasztani.
Ennek megfelelően azt tartjuk majd számon, hogy melyik számtartományban lehet a megoldás, a következő tippet pedig mindig ennek a tartománynak a közepére választjuk.

program szamkitalalos;
var also,felso,tipp,valasz:integer;
begin
  also:=1;felso:=100;  { az intervallum kezdeti hatarai }
  writeln('Gondolj egy egesz szamot 1 es 100 kozott!');
  repeat
    tipp:=(also+felso) div 2;  { kozepso szam }
    writeln('Kovetkezo tipp: ',tipp);read(valasz);
    if valasz=0 then writeln('Eltalaltam!')
    else if valasz=-1 then felso:=tipp-1  { kisebb }
      else also:=tipp+1; { nagyobb }
    if also>felso then begin
      writeln('Te csalsz!');
      valasz:=0
    end
  until valasz=0  { talalt }
end.

E programban új alsó, illetve felső határnak nem egyszerűen a tipp értékét vettük, hanem annál eggyel nagyobb, illetve kisebb értéket. Ellenkező esetben ugyanis, például ha 100-ra gondolunk, ezt a gép nem tudja kitalálni. Ennek az az oka, hogy a következő tipp kiszámításánál also+felso értéke 199, ha pedig ezt 2-vel osztjuk, akkor a div művelet lefelé kerekít, tehát megint csak 99-et kapunk. Könnyen találhatunk további olyan számokat is, amelyeket a +1, -1 nélküli program nem tudna kitalálni.
Más okból is érdemes eszerint eljárni: ha a tippünk nem talált, akkor érdemes e tipp értékét is kizárni a lehetséges számok tartományából, tehát így valamivel gyorsabban szűkül az intervallum.

2.11.1. Feladat:
Egészítse ki az előbbi programot úgy, hogy valóban csak az 1, 0, illetve -1 válaszokat fogadja el!

Az előbbi program használójának kellemetlen, hogy válaszait a gép számára számszerűsítenie kell. Tehát: például amikor a válasz az, hogy kisebb, akkor nem ezt a szót vagy egy k betűt kell beírnia, hanem azt, hogy -1.

2.11.1. Példa:
Alakítsuk át a számkitaláló programot úgy, hogy a kisebb, nagyobb, illetve talált válaszokat a 'k', 'n', illetve 't' betűk beírásával lehessen megadni!

Megoldás:
Ehhez olyan adattípusra van szükségünk, amely karakter tárolására képes. Ez a karakter, azaz char típus.
A program karakter típus használatára átalakított változata:

program szamkitalalos2;
var also,felso,tipp:integer;
    valasz:char;
begin
  also:=1;felso:=100;  { az intervallum kezdeti hatarai }
  writeln('gondolj egy egesz szamot 1 es 100 kozott!');
  repeat
    tipp:=(also+felso) div 2;  { kozepso szam }
    writeln('Kovetkezo tipp: ',tipp);
    readln(valasz);
    if valasz='t' then writeln('Eltalaltam!')
    else if valasz='k' then felso:=tipp-1   { kisebb }
      else if valasz='n' then also:=tipp+1; { nagyobb }
    if also>felso then begin
      writeln('Te csalsz!');
      valasz:='t'
    end
  until valasz='t'  { talalt }
end.

HiSoft-Pascalban readln(valasz); helyett ezt kell írni: readln;read(valasz);
A program működése az eddigiek alapján könnyen kitalálható, a char típusra vonatkozó szabályok itt következnek:


A karakter (char) típus

1. A karakter típus a szövegek írásakor használt jel, tehát betű, számjegy, írásjel, szóköz, egyéb speciális jel ábrázolását teszi lehetővé. Előre definiált, azaz szabványos típus, előre definiált típusazonosítója char.

Az adott megvalósítás karakterkészlete, azaz a használható karakterek tartománya és az egyes karakterek értelmezése megvalósításban definiált. (a)
Léteznek olyan speciális, úgynevezett vezérlő karakterek is, amelyek a számítógépek adatbeviteli eszközein (billentyűzet) általában nem írhatók be, illetve adatmegjelenítő berendezésein (képernyő, nyomtató) közvetlenül nem írhatók ki. (b)

Egy karakter egyetlen jel tárolását teszi lehetővé, többkarakteres szövegeket karakterekből álló tömbökben tudunk tárolni.

2. Karakteres konstans írásmódjának formai szabályai:

karakterkonstans:

aposztrófmegadás: ''

A karaktert tehát aposztrófok között kell megadnunk, ha pedig magát az aposztrófot akarjuk konstansként megadni, azt dupláznunk kell.
Helyes karakterkonstansok:

'a' - kis a betű,
' ' - szóköz,
'"' - idézőjel,
'2' - kettes számjegy,
'''' - aposztróf

Hibásak:

'' - az aposztrófok között nincsen semmi (tehát üres karaktert nem lehet megadni),
''' - ha aposztrófot akarunk megadni, azt duplázni kell,
'ab' - két karakter van az aposztrófok között,
'c ' - két karakter van itt is, c és szóköz,
'z - hiányzik a záró aposztróf,
"q" - aposztrófot, nem pedig idézőjelet kell használni,
'a' - aposztróf helyett "accent grave" karaktert használtunk (ASCII-ben ord(''')=96)

3. A karakter sorszámozott típus, ord értéke megvalósításban definiált, nem lehet negatív.
Minden megvalósításban teljesülniük kell a következő szabályoknak:

3.1. Az angol ábécé szerint előbb álló kisbetű ord értéke kisebb a hátrább álló kisbetűénél, az előbb álló nagybetű ord értéke kisebb a hátrább álló nagybetűénél:

ord('a') < ord('b') < ord('c') < ... < ord('z')

illetve

ord('A') < ord('B') < ord('C') < ... < ord('Z')

Ez nem jelenti azt, hogy az angol ábécé szerint egymást követő betűk ord értékei pontosan eggyel különböznének egymástól. (c)

3.2. A számjegyek ord értékei folytonos sorozatot alkotnak:

ord('0')+1=ord('1')
ord('1')+1=ord('2')
...
ord('8')+1=ord('9')

Így bármely k számjegykarakternek megfelelő számérték kiszámítható az ord(k)-ord('0') kifejezéssel.

3.3. Az egyéb karakterek (pl. írásjelek, szóköz) ord értékeinek viszonya a szabványban nem rögzített.

3.4. A szabvány nem rögzíti a kisbetűk csoportjának, a nagybetűk csoportjának, a számjegyek csoportjának és az egyéb jeleknek egymáshoz viszonyított helyzetét ord érték szempontjából. (d)

4. Karakter típusú adattal a következő műveletek végezhetők:

5. A karakterek összehasonlításának eredménye megegyezik ord értékeik összehasonlításának eredményével. Ha tehát ch1 és ch2 is char típusú, akkor pl. ch1<ch2 akkor és csak akkor igaz, ha ord(ch1)<ord(ch2). (e)

Az összehasonlítás eredménye boolean.

Karakterekre alkalmazható összehasonlító műveletek:

= egyenlő
<> nem egyenlő
< kisebb
> nagyobb
<= kisebb vagy egyenlő
>= nagyobb vagy egyenlő

6. Karakterek függvényei (a ch-val jelölt paraméter char, a k-val jelölt pedig integer típusú):

Magyarázatok: megjegyzések:

(a) A különböző gépek szinte kizárólag a két legelterjedtebb karakterkészlet, az ASCII (Americi Standard Code for Information Interchange) vagy az EBCDIC (Extended Binary Coded Decimal Interchange Code) valamelyikét használják.
A szabványos Pascal nyelvben a karakterkészletre tett megkötéseket mind az ASCII, az EBCDIC karakterkészlet teljesíti, nem teljesíti azonban pl. a CCITT Nr.2, közismert nevén Telex kód.

(b) Az ASCII karakterkészletben ilyen vezérlőkarakter például a 12-es ord értékű lapdobás (Form Feed) karakter, amely egyes nyomtatóknál azt jelenti, hogy ugorjunk a következő lap elejére. Mivel a vezérlőkarakterek jelentése függ a megvalósítástól, használatukat a Pascal programok hordozhatósága érdekében kerülni kell.

(c) Az ASCII kódban a kisbetűk csoportja és a nagybetűk csoportja külön-külön folytonos sorozatot alkot, azaz a saját csoportján belül az angol ábécében egymást követő betűk ord értékei eggyel térnek el egymástól.

Az EBCDIC kódban mind a kisbetűk, mind a nagybetűk csak az 'a'...'i', 'j'...'r', 's'...'z' tartományokban alkotnak összefüggő sorozatot.

(d) ASCII kódban:

számjegyek < nagybetűk < kisbetűk

EBCDIC kódban:

kisbetűk < nagybetűk < számjegyek

Az egyéb jelek, pl. írásjelek mindkét karakterkészletben keverten helyezkednek el az előbbi csoportok között, az EBCDIC-ben még a betűk csoportjai között is, a szóköz (space) azonban mindkét karakterkészletben az összes többi nyomtatható karakternél kisebb ord értékű.
Ezeket a tulajdonságokat a programok hordozhatósága érdekében nem érdemes kihasználni.

(e) Mivel két karakter összehasonlításának eredményét éppen ord értékeik összehasonlításának eredményével definiáltuk, nem érdemes, sőt kerülendő a karakterek ord értékeit hasonlítgatni. Nemcsak hosszabb, de nehezebben is olvasható például a

while(ord(ch)>=ord('0')) and (ord(ch)<=ord('9')) do ...

programrészlet a

while (ch>='0') and (eh<='9') do ...

helyett.


2.11.3. Példa:
Készítsünk programot, amely kiírja gépünk karakterkészletét!

Megoldás:
Mivel a számítógépek túlnyomó többsége egy karaktert egy byte-on, (kiejtése: bájt) azaz 8 bites memóriaterületen tárol, ez elvileg

2^8 = 256

karaktert tesz lehetővé. Az áttekinthetőség kedvéért a karaktereket 16*16-os táblázat formájában írjuk ki, az oszlopot a byte bal oldali 4 bitje, a sort pedig a jobb oldali 4 bitje adja meg. Így a karakter ord értékét a bal félbyte értékének 16-szorosa, plusz a jobb félbyte értéke adja.
A sorok elé és az oszlopok fölé írjuk ki az adott félbyte számértékét, az oszlopok fölé pedig a bal félbyte helyiértékének megfelelő értéket is!

program karakterkeszlet;
var bal,jobb:integer;
  begin
  writeln('Karakterkeszlet:');writeln;write(' ');
  for bal:=0 to 15 do write(bal:4);
  writeln;write(' ');
  for bal:=0 to 15 do write(16*bal:4);
  writeln;writeln;
  for jobb:=0 to 15 do begin
    write(jobb:2,'      ');
    for bal:=2 to 9 do
      write(chr(16*bal+jobb):4);
    writeln
  end
end.

Ha ezzel a programmal kiíratjuk gépünk karakterkészletét, akkor ilyesféle táblázatot kapunk:

Azért csak "ilyesféle", mert bizonyos karakterek, amelyeket vezérlőkaraktereknek nevezünk, befolyásolják a kiírás módját. Nagyon gyakori például, hogy a 12 ord értékű karakter a képernyő törlését, illetve nyomtatón a következő lap elejére ugrást okozza (Form Feed). Ezeket a karaktereket (0-16 értékűek) a programból is kihagytuk.
Ha gépünk az ASCII kódkészletet használja, akkor ajánlatos csak a 32 és 127 közötti ord értékű karaktereket kiíratni. Ebben a tartományban rendszerint nincsenek vezérlőkarakterek. Az előbbi programban ehhez a bal változó értékét 2-től 7-ig kell léptetni.
PC-n próbálkozhatunk a 32-255 tartomány kiírásával, ehhez a bal változó értékét 15-ig kell léptetni.

2.11.4 Példa:
Készítsünk programrészletet, amely karakterenként olvassa a bemeneti adatokat egészen addig, amíg egy - esetleg előjeles - egész számot nem talál, majd ennek meghatározza az értékét. Egy ilyen programrészletre akkor lehet szükség, ha egész értéket szeretnénk beolvasni, de előtte nem csak szóközök, illetve újsor jelek vannak, hanem valami más szöveg is. (Számot szeretnénk beolvasni, de a program használója figyelmetlen volt.)
Ekkor read(k) használata esetén a program hibaüzenettel leáll, és ez néha nagyon kellemetlen.
A programrészlettől tehát azt várjuk, hogy például az

adat=-235

megadott bemeneti adat esetén hagyja figyelmen kívül az "adat=" szövegrészt, csak a -235 számot vegye figyelembe, a beolvasott karakterekből pedig határozza meg azok értékét, és akkor fejezze be a működést, ha a szám olvasását befejezte.
A programrészlet általános szerkezete:

karakterek beolvasása, amíg számjegy nem jön;
számjegyek beolvasása és a szám értékének meghatározása;

Ebben a vázlatban nem foglalkoztunk a szám előjelével. Ezt úgy vehetjük figyelembe, hogy megjegyezzük az első számjegy előtti karaktert, majd amikor a számjegyekből kiszámítottuk a szám értékét, a szám előtti karakter esetén a kapott érték mínusz egyszeresét vesszük.
Egy szám értékét a következőképpen lehet a számjegyek értékéből kiszámítani: amikor egy újabb számjegyet olvastunk be, a szám eddigi értékét megszorozzuk 10-zel és ehhez adjuk hozzá a következő számjegyértékét, pl.:

A szám eddigi értéke
Következő karakter
A szám új értéke
0
2
10 * 0 + 2 = 2
2
3
10 * 2 + 3 = 23
3
5
10 * 23 + 5 = 235

A programrészlet, amit eljárás formájábanadunk meg:

procedure readint(var ertek:integer);
var ch,elojel:char;
begin
  ch:=' ';ertek:=0;
  repeat
    elojel:=ch; { elozo karakter megjegyzese }
    read(ch)    { kovetkezo karakter }
  until (ch in ['0'..'9']); { szamjegy jott }
  repeat { szam beolvasasa }
    ertek:=10*ertek+ord(ch)-ord('0'); { uj ertek }
    read(ch)    { kovetkezo karakter }
  until not(ch in ['0'..'9']); { nem szamjegy jott }
  if elojel='-' then ertek:=-ertek  { elojel figyelembevetele }
end;

Ugyanez HiSoft-Pascalban:

procedure readint(var ertek:integer);
var ch,elojel:char;
begin
  ch:=' ';ertek:=0;
  repeat { szam elotti karakterek atugrasa }
    elojel:=ch; { elozo karakter megjegyzese }
    read(ch)    { kovetkezo karakter }
  until (ch in ['0'..'9']) or eoln; { szamjegy jott vagy... }
  if not eoln then { ...nincs tobb karakter? }
    repeat { szam beolvasasa }
      ertek:=10*ertek+ord(ch)-ord('0');    { ertek hozzaadasa }
      read(ch)  { kovetkezo karakter }
    until (not(ch in ['0'..'9'])) or eoln; { nem szamjegy jott }
  if ch in ['0'..'9'] then  { utolso szamjegyet meg hozzaadjuk }
      ertek:=10*ertek+ord(ch)-ord('0');
  if elojel='-' then ertek:=-ertek   { elojel figyelembevetele }
end;

A fenti programrészlet Turbo Pascal-ban nem működik a read eljárás eltérő működése miatt, de ott sokkal kényelmesebb megoldások is vannak a probléma megoldására.

2.11.5. Példa:
Alakítsuk át az előbbi programrészletet úgy, hogy a beolvasott számot nem tízes, hanem 16-os számrendszerbelinek tekintjük! A tizenhatos, idegen szóval hexadecimális számrendszerben a 0...9 számjegyeket és az A...F betűket használjuk számjegyeknek a következő megfeleltetéssel:

Hexadecimális számjegyek: 0 1 2 3 4 5 6 7 8 9 A B C D E F
Decimális számjegyek: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Például:

Hexadecimális decimális  
5 =
5  
F =
15  
10 =
16 = 1*16 + 0
1A =
26 = 1*16 + 10
32 =
50 = 3*16 + 2
13A =
314 = 1/256 + 3*16 + 10

Megjegyzendő, hogy a szabványos Pascal nyelvben nincs is más lehetőség hexadecimális szám beolvasására, mint a most következő módszer.
Az egyszerűség kedvéért először tételezzük fel, hogy gépünk karakterkészletében nemcsak a decimális számjegyek, hanem az A...F betűk is folytonos sorozatot alkotnak, azaz

ord('B')=ord('A')+1 és ord('C')=ord('B')+1 stb.

A megfelelő programrészlet:

procedure readhex(var ertek:integer);
var ch,elojel:char;
begin
  ch:=' ';ertek:=0;
  repeat { szam elotti karakterek atugrasa }
    elojel:=ch; { elozo karakter megjegyzese }
    read(ch)    { kovetkezo karakter }
  until (ch in ['0'..'9','A'..'F']) { hexa szamjegy jott}
  repeat { szam beolvasasa }
    if ch in ['0'..'9'] then ertek:=16*ertek+ord(ch)-ord('0')
    else ertek:=16*ertek+ord(ch)-ord('a')+10;
    read(ch)  { kovetkezo karakter }
  until (not(ch in ['0'..'9','A'..'F']));
  if elojel='-' then ertek:=-ertek  { elojel figyelembevetele }
end;

Ugyanez HiSoft-Pascalban:

procedure readhex(var ertek:integer);
var ch,elojel:char;
begin
  ch:=' ';ertek:=0;
  repeat { szam elotti karakterek atugrasa }
    elojel:=ch; { elozo karakter megjegyzese }
    read(ch)    { kovetkezo karakter }
  until (ch in ['0'..'9','A'..'F']) or eoln; { hexa szamjegy }
  if not eoln then
    repeat { szam beolvasasa }
      if ch in ['0'..'9'] then ertek:=16*ertek+ord(ch)-ord('0')
      else ertek:=16*ertek+ord(ch)-ord('a')+10;
      read(ch)  { kovetkezo karakter }
    until (not(ch in ['0'..'9','A'..'F'])) or eoln;
  if ch in ['0'..'9'] then ertek:=16*ertek+ord(ch)-ord('0');
  if ch in ['A'..'F'] then
      ertek:=16*ertek+ord(ch)-ord('a')+10;
  if elojel='-' then ertek:=-ertek  { elojel figyelembevetele }
end;

A betűk ord értékére tett feltételezés miatt elég kényelmesen ki tudtuk számítani az A...F tartományba eső karakternek megfelelő értéket, és ez a feltételezés az ASCII és EBCDIC karakterkészletre is teljesül.
Mit tehetünk akkor, ha pontosan a nyelv szabványa szerint akarunk eljárni, tehát nem használhatjuk ki ezt a tulajdonságot? Nos, ekkor az A...F betűket külön-külön kell figyelembe venni a szám új értékének meghatározásakor, például így:

...
ertek:=16*ertek;
if ch in ['0'..'9'] then
  ertek:=ertek+ord(ch)-ord('0')
else if ch='A' then ertek:=ertek+10
  else if ch='B' then ertek:=ertek+11
    else if ch='C' then ertek:=ertek+12
      else if ch='D' then ertek:=ertek+13
        else if ch='E' then ertek:=ertek+14
          else if ch='F' then ertek:=ertek+15;
...

Ez hosszadalmas és nehezen olvasható. Olyan utasításra lenne szükség, amely kényelmes módon lehetőséget ad többirányú elágazásra, azaz arra, hogy ne csak két eset közül tudjunk választani.
Ez a case utasítás. Az előbbi programrészlet a case utasítás felhasználásával:

...
ertek:=16*ertek;
if ch in ['0'..'9'] then
  ertek:=ertek+ord(ch)-ord('0')
else
  case ch of
    'A': ertek:=ertek+10;
    'B': ertek:=ertek+11;
    'C': ertek:=ertek+12;
    'D': ertek:=ertek+13;
    'E': ertek:=ertek+14;
    'F': ertek:=ertek+15;
end;
...

Lehetőség van arra is, hogy a 0...9 karaktereket is a case utasításon belül vegyük figyelembe:

...
ertek:=16*ertek;
case ch of
  '0','1','2','3','4','5','6','7','8','9':
       ertek:=ertek+ord(ch)-ord('0');
  'A': ertek:=ertek+10;
  'B': ertek:=ertek+11;
  'C': ertek:=ertek+12;
  'D': ertek:=ertek+13;
  'E': ertek:=ertek+14;
  'F': ertek:=ertek+15;
end;
...

A case utasítás

1. Alakja:

case utasítás:

 

2. A case utasítás végrehajtása: a gép kiértékeli a kifejezés értékét, majd azt és csak azt utasítást hajtja végre, amely előtt a kifejezés értékével megegyező konstans áll.

3. Ha a kifejezés értéke egyik konstanssal sem egyezik meg, hiba történik. Emiatt a programozónak gondoskodnia kell arról, hogy a kifejezés értéke biztosan megegyezzen valamelyik konstanssal.

4. A kifejezésnek és a konstansoknak azonos sorszámozott típusúaknak kell lenniük. (a)

5. Az utasításban felsorolt konstansok értékének mind különbözőnek kell lennie. (b)

(A legtöbb megvalósítás - így a Turbo Pascal és HiSoft-Pascal is - a case utasítást az else ág használatának lehetőségével bővíti. Mivel az else ág használata case utasításban nem szabványos lehetőség, szintaktikai szabályai megvalósításfüggőek.)

Magyarázatok, megjegyzések:

(a) A 4. szabályból következik, hogy a case utasítással több irányban elágazni csak integer, char, boolean és felsorolt típus alapján lehet.

Egy elágazás integer típus szerint:

case k*2-1 of
  1: ...;
  3,7,9: begin ...; ...; end
  6,38: ... { sohasem hajtódik végre! }
end;

Az előbbi példában szereplő szemantikai hibát a fordítóprogram nem képes felderíteni, hiszen nem ismerheti fel, hogy a kifejezés értéke mindig páratlan.

Egy elágazás char típus szerint:

case ch of
  'A','E',I','O','U': ...;
  'Z','T','V',: ...
end;

Egy elágazás boolean típus szerint:

case a>b of
  true: ut1;
  false: ut2
end;

Ezt az esetet if utasítással sokkal természetesebben kezelhetjük:

if a>b then ut1 else ut2;

Egy elágazás felsorolt típus szerint:

case nap of
  he,sze,pe: ...;
  ke,cs:...
end;

Itt feltételeztük a következő definíciót ill. deklarációt:

type naptipus=(he,ke,sze,cs,pe,szo,va);
var nap:naptipus;

Ebben a példában hiba történik, ha a nap értéke szo vagy va.

(b) Az 5. szabály értelmében tehát szintaktikailag hibás a következő utasítás, mert a felsorolásban az 1 ismétlődik:

case g of
  1: ...;
  3,6,55,4: ...;
  11,1: ...
end;


2.11.2. Feladat:
Alakítsa át a hexadecimális számokat beolvasó programrészletét úgy, hogy akkor is helyesen működjön, ha az a..f karaktereket (kisbetűket) adunk meg.

2.11.3. Feladat:
Készítsen öröknaptárat: olyan programot, amely az év, hónap, nap formában megadott dátumról megadja, hogy az a hét melyik napjára esett!

2.11.4. Feladat:
Készítsen programrészletet, amely valós szám beolvasását végzi!

2.12. Konstans definiálása

A 2.9. alfejezetben az egyenletek közelítő megoldásakor a megengedett hiba értékét egy hibahatar változó adta meg. Ez ténylegesen csak egyszer kapott értéket, mégpedig a program legelején. Hasonló szerepe volt a relativhiba változónak is a 2.10. alfejezetben a numerikus integrálás pontosságának megadásakor. Ezek a változók tehát valójában nem "változó", hanem konstans értékeket tároltak. Azért használtunk mégis változót e számok megadására, mert a program így jobban érthető, sőt, ha később egy másik feladathoz meg kell változtatni a hibahatár értékét - és azt a programban több helyen is használjuk - a változtatást csak egy helyen kell elvégezni: a hibahatárt megadó változó értékének kezdeti beállításakor.
Jobb lenne azonban, ha erre a célra nem változót, hanem valódi konstanst tudnánk definiálni, azaz névvel tudnánk ellátni általunk megadott konstans értékeket. Erre a konstansdefiníció ad lehetőséget.

Az intervallumfelezes2 programban például a hibahatárt konstanssal a következő módon adhatjuk meg:

program intervallumfelezes3;
const hibahatar=1E-6;
var a,b,x:real;
    novekvo:boolean;
begin
  a:=-2;b:=5;
  novekvo:=exp(a)+5*a-5<0;
  repeat
    x:=(a+b)/2;
    if novekvo=(exp(x)+5*x-5<0) then a:=x
    else b:=x
  until b-a<hibahatar;
  writeln('Az egyenlet megoldasa:  x=',x:10:6)
end.

Mint a példából is látható, konstanst a változók deklarációja előtt definiálhatunk, a konstansdefiníciós rész elejét a const kulcsszó jelzi. A konstans nevének megadása után egyenlőségjel, majd az érték megadása következik. Egy konstansdefiníciós részben több konstanst is definiálhatunk, pl.:

const
  pi=3.14159265358;
  minuszpi=-pi;
  ezer=1000;
  igaz=true; hamis = false;
   nagyAbetu='A'; kisabetu='a'; aposztrof='''';
  hibauzenet=' ***** hiba *****';

A pi nevű konstans a matematikából ismert értéket adja meg, az igaz, illetve hamis konstansok pedig logikai, azaz boolean típusúak. Lehet karakteres, illetve karakterlánc típusú konstansokat is definiálni, ezek értékét a char típusnál megszokott, illetve a write-ban látott módon adhatjuk meg.

Konstans definiálásakor nem kell megadni a konstans típusát, azt a gép az egyenlőségjel után álló érték alapján határozza meg.
Az így definiált konstansneveket a program további részében mindenütt használhatjuk, ahol konstans állhat. Különösen ajánlott konstansok használata tömbök indexhatárainak megadásakor. (A tömbökkel majd a következő fejezetben találkozunk.)

Előnyös tulajdonságuk még a konstansoknak, hogy értéküket véletlenül sem lehet a program végrehajtása során megváltoztatni. Szintaktikai hibát okoz ugyanis, ha egy konstans azonosítója értékadó utasítás baloldalán fordul elő, vagy bármilyen más módon, például read utasítással próbáljuk értékét megváltoztatni. Az előbbi definíciók esetén például:

pi:=1; { hiba: konstansnak nem adhato ertek }
ezer:=1000; {hiba akkor is, ha ugyanaz lenne }
read(hinaüzenet); {hiba: konstans nem olvashato be }

Konstansnevek definiálása és használata a következő esetekben ajánlható:

Stílus:
Ha csak lehet, érdemes konstansainkat névvel ellátva használni és a program további részében e nevekre hivatkozni. Ha később meg kell változtatni egy konstans értékét, akkor azt kényelmesen és biztonságosan megtehetjük, hiszen csak egy helyen, a definíciónál kell a programon változtatni.
Ügyeljünk a megfelelő névválasztásra! A gép nem tudja ellenőrizni, hogy az általunk használt konstansnál - hasonlóan a változók nevéhez - megfelel-e a jelentésének. így definiálhatnánk ilyen konstansokat is:

const
  igaz=false;
  tiz=-10;

Sőt, lehetőségünk van arra is, hogy az előre definiált konstansok nevével megegyező nevűeket definiáljunk. Ilyenkor programunk további részében - pontosabban a konstansdefiníciót tartalmazó blokk további részében - a konstans azt az értéket jelenti, amit megadtunk.
Például:

const
  maxint=1000;
  true=false; { nem ajanlott! }

Ilyet azonban gyakorlatilag soha nem érdemes tenni, mert előbb-utóbb saját magunkat vezetjük félre, általában nagyon nehezen felderíthető hibákat okozva.
Vigyázni kell arra is, hogy konstansnak és változónak ne adjunk ugyanolyan nevet, hiszen ez ellentmondásra vezet: a gép nem tudná eldönteni erről az azonosítóról, hogy a konstanst vagy a változót jelenik Az ilyen dupla definíció egyébként szintaktikai hibát jelent és a fordítóprogram jelzi.


Konstansdefiníció

1. A konstansnév olyan azonosító, amely állandó értéket képvisel, azaz értéke nem változtatható meg a program végrehajtása során.

2. Alakja:

azonosító = konstans

Ahol a konstans lehet előjel nélküli szám, előjeles szám, karakterkonstans, karakterlánc-konstans, előzőleg definiált konstansazonosító

3. Egész, valós, logikai, felsorolt, karakter és karakterlánc típusú konstans definiálható. (a)
Az adott konstansazonosító által képviselt érték típusát az adja meg, hogy definíciójában az egyenlőségjel után milyen típusú értéket adtunk meg. Például:

4. Ha a konstans értékének megadásában az előjel után azonosító áll, az csak egész vagy valós típusú konstans azonosítója lehet. (c)

5. Ha a konstans azonosítójára hivatkozunk, a definiáláskor megadott értéket kapjuk meg.

Magyarázatok, megjegyzések:

(a) Konstansdefiníciós részben felsorolt típusú konstans csak előzőleg már definiált konstansazonosító segítségével definiálható.
Például:

...
type eset=(kisebb,egyenlo,nagyobb);
...
procedure p;
const azonos=egyenlo;

A fenti programrész azonos-t, mint eset típusú konstanst definiálja.

(b) Konstans értékének definiálásában nem lehet kifejezést használni, még akkor sem, ha a kifejezés csak konstansokra hivatkozik. így nem megengedettek a következő definíciók sem:

nyolck=8*1024
ketpi=2*pi
kocsivissza=chr(13)

Több megvalósítás megengedi ezeket, de ha kihasználjuk, akkor programunk nem lesz hordozható.

(c) Az előre definiált konstansok:

maxint (integer típusú)
false, true (boolean típusú)

2.13. Az adatbeolvasás és adatkiírás elemei

Eddigi mintaprogramjainkban is szükség volt arra, hogy beolvassunk, illetve kiírjunk adatokat. Most összefoglaljuk azokat a szabályokat, amelyek a további feladatok megoldásához szükségesek. Ennek a témának a további részleteit majd az adatállományok, azaz file-ok (kiejtése: fájl) tárgyalásakor ismerjük meg.

Kiírás: write, writeln, page
Az adatok kiírása - hacsak más utasítást nem adunk - úgynevezett szabványos kimeneti adatállományba történik, ennek output a programbeli neve. Az output állományba írás személyi számítógépeknél tipikusan a képernyőre mint output eszközre való írást jelent, de jelentheti pl. valamilyen nyomtató használatát is. Hogy az adott gépen az adott Pascal rendszert használva pontosan milyen eszköznek felel meg a szabványos output állomány, azt az adott Pascal megvalósítás kézikönyve adja meg. Ebben az alfejezetben a kiírási műveletek mind a szabványos output állományra vonatkoznak.

A kiírást a write, writeln, page, put és rewrite előre definiált eljárásokkal vezérelhetjük, ezek közül most csak az első hárommal foglalkozunk.

A write eljárás:
Adottak például a következő deklarációk:

var c:char;
  k:integer;
  v:real;

Ekkor

write(' Eredmenyek : ',c:5,k-3,v:9:2,v<k:5)

hatására a gép az ' Eredmények : ' szövegkonstans, a c karakter, a k-3 egész kifejezés, a v valós változó és a v<k logikai kifejezés értékét, tehát a zárójelek között megadott értékeket írja ki, az ott megadott sorrendben, ugyanabba a sorba. Például így:

Eredmelyek :     x   -331.66 true

A write eljárás hívásában legalább egy paraméternek lennie kell, de lehet több is, ezeket vesszővel választjuk el egymástól.

A write végrehajtása után újabb write vagy writeln hatására a kiírás ugyanabban a sorban, a következő karakternél folytatódik, tehát ott, ahol az előző write befejezte.
A kiírás alakja attól függ, hogy milyen típusú adatot akarunk kiíratni és milyen kiírási formát adunk meg. Ha nem adunk meg ilyen formai előírást, akkor a gép az alapértelmezés szerinti alakot használja. Ez megvalósításfüggő, tehát az adott Pascal rendszer kézikönyvéből tudhatjuk meg. A kiírás alakját a kettőspont utáni értékkel vagy értékekkel írhatjuk elő, ezek legalább 1 értékű egész mennyiségek lehetnek. Az első kettőspont utáni mennyiség a mezőhosszt adja meg, tehát azt, hogy az előtte álló értéket hány karakter hosszúságban írja ki a gép. A mezőhosszt a továbbiakban m-mel jelöljük. Valós mennyiség kiírásakor újabb kettőspont után a törtrész hossza is megadható, ezt t-vel fogjuk jelölni.

Az is látható az előbbi példából, hogy nemcsak változó, illetve konstans, hanem kifejezés is megadható a kiírandó értékek között.

A továbbiakban a kiírás szabályait adattípusonként részletezzük.

Karakter kiírása:

write(ch) hatására az itt ch-val jelzett egyetlen karakter íródik ki,
write(ch:m) hatására pedig m-1 szóköz, majd eh értéke.

Karakterlánc (string) kiírása:

write(s) az s karakterláncot írja ki annyi karakteren, amekkora s hossza.
write(s:m)
az s karakterláncot m karakter szélességen írja ki. Ha m nagyobb, mint az s karakterlánc h hossza, akkor először m-h szóköz íródik ki, majd az s string. Ha h>m, akkor az s karakterláncnak csak az első m karaktere jelenik meg. (A Turbo Pascal és a HiSoft-Pascal ez utóbbi esetben azonban, figyelmen kívül hagyja a megadott mezőszélességet, azaz olyan mezőszélességet használ, ami elegendő a karakterlánc kiírásához.)

Egész érték kiírása:

write(k) k értékét írja ki decimális, azaz tízes számrendszerben, annyi karakternyi helyen, amennyit az alapértelmezés előír.
Ha a szám negatív, akkor az első számjegy elé mínusz előjelet, ha pozitív vagy nulla, akkor szóközt tesz.
write(k:m)
k értékét m karakter szélességben írja ki, azaz a szám értéke előtt annyi szóköz lesz, hogy a teljes hosszúság éppen m legyen. Ha m kisebb, mint ami a szám kiírásához kell, akkor is kiírja a teljes számot, előtte az előjelnek megfelelően mínuszjellel vagy szóközzel, tehát m-nél nagyobb mezőszélességet használ.

Valós érték kiírása:

write(v)
v értékét írja ki normalizált, kitevős alakban, az alapértelmezés szerinti pontosággal. Ez általában megfelel annak, amilyen pontosan a gép a valós számokat tárolja. Előtte a szám előjelének megfelelően szóköz vagy mínuszjel lesz
Ha a pontosság például 11 decimális jegy - mint a Turbo Pascal-ban -, és a kitevő kétjegyű, akkor write(exp (1)) hatására

2.7182818285E+00

íródik ki.

write(v:m)
v értékét az előzőhöz hasonló normalizált, kitevős alakban írja ki m karakter szélességben, azaz a törtrészből annyi számjegyet ír ki, amennyi az előjel az egészrész, a tizedespont és a kitevőrész számára szükséges helyen kívül marad, de legalább egy számjegyet a törtrészből is mindig kiír.
Például write(-exp(1):10) hatására

-2.718E+00

íródik ki, write(-0.1234:3) hatására pedig

-1.2E-01

write(v:m.t)
v értékét fixpontos alakban írja ki, m karakter szélességben, ezen belül a törtrészt t számjeggyel. Az egészrészre az előjel és a tizedespont miatt m-t-2 számjegy jut. Ha az egészrész ennél rövidebb, a szám elé szóközök kerülnek. Például write(123.1234:10:3) hatására

123.123

jelenik meg. Ha az egészrész számára nem maradna elég hely, akkor is kiíródik a teljes szám az előjelnek megfelelő mínuszjellel, illetve szóközzel együtt. Például write(123456.123:5:2) hatására

123456.12

íródik ki.

Logikai érték kiírása:

write(b)
a b logikai értéknek megfelelően a true vagy false szöveg íródik ki. Megvalósítástól függ, hogy milyen szélességben és hogy kis- vagy nagybetűsen.
write(b:m) a b logikai értéket írja ki m karakteren a karakterláncokra vonatkozó szabályok szerint.

A writeln eljárás:
A writeln eljárás nagyon hasonló a write eljáráshoz, két különbséggel:

Példa:

writeln('Egy');
for k:=1 to 5 do write(k:3);
writeln('Ketto');
for k:=1 to 4 do writeln(k:3);
writeln;
writeln('Harom');

eredménye:

A sorvége jel, amit a writeln eljárás kiír, nem tartozik hozzá a Pascal nyelv karakterkészletéhez, tehát nincs ord értéke sem. A Pascal nyelven programozó számára az is érdektelen, hogy ez valójában egy karakterhez hasonló jel kiírását jelenti-e, vagy valamilyen más műveletet, például egy olyan parancs végrehajtását, amire a nyomtatón a papírt a gép egy sorral feljebb emeli.

A page eljárás:
Pontos hatása megvalósításban definiált. Tipikus azonban, hogy utána a kiírás a következő lap elején, illetve üres képernyőn folytatódik. Nincs paramétere.
(A page eljárás a Turbo Pascalban nem implementált, helyette pl. képernyőtörlése a ClrScr eljárást használhatjuk.)

Beolvasás: read, readln, eoln, eof
Az adatok beolvasása, hacsak más utasítást nem adunk, az úgynevezett szabványos bemeneti állományról történik. Ennek input a programbeli neve, ezt is meg lehet adni a program fejrészében. Az input állományról olvasás a személyi számítógépeknél tipikusan a billentyűzetről való adatbevitelt jelenti, de lehet ez valamilyen más adatbeviteli eszközről, például mágneslemezről való beolvasás is. Hogy az adott gépen az adott Pascal rendszert használva pontosan milyen eszközt jelent a szabványos input állomány, az a megvalósítástól függ, tehát a megvalósítás kézikönyve adja meg (A Turbo Pascal figyelmen kívül hagyja a program fejlécében megadott paramétereket, a HiSoft-Pascalban pedig hibát okoz, el kell hagyni). Az ebben a fejezetben tárgyalt műveletek mind a szabványos input állományra vonatkoznak.
A beolvasáshoz a szabványos eoln és eof függvény és a read, readln, reset és get eljárás áll rendelkezésünkre, közülük most csak az első négyre van szükségünk.

A read eljárás:
Adottak a következő deklarációk:

var c:char;
  k:integer;
  v:real;

ekkor

read(c,k,v)

hatására a c, k és v változókba olvasunk be értékeket, ilyen sorrendben. Legalább egy paraméternek lennie kell, de lehet több is, ezeket vesszővel választjuk el egymástól. A paraméterek karakteres, egész vagy valós változók lehetnek.
A read végrehajtása után újabb read vagy readln hatására a beolvasás ugyanabban a sorban a következő karakternél folytatódik, tehát ott, ahol az előző read abbahagyta.

Karakter beolvasása:

read(ch)
hatására az itt ch-val jelzett, karakter típusú változóba a gép beolvassa a bemeneti adatok következő karakterét. Ha éppen sorvége jel következik, akkor ch értéke szóköz lesz. (Ezért, illetve az eoln eljárás működésének különbsége miatt kellet a readhex és readint eljárásokat HiSoft-Pascalban másképp elkészíteni.) Hiba történik, ha nincs több beolvasható karakter az adatok végéig.

Egész szám beolvasása:

read(k)
hatására a gép a k-val jelölt egész típusú változóba beolvassa a bemeneti adatokból a következő egész számot. A beolvasás pillanatnyi pozíciója és a szám között tetszőleges számú szóköz és újsor jel lehet, ezeket a gép figyelmen kívül hagyja. Hiba történik, ha az előbbiektől eltérő karakter is van a következő számig, vagy nincs egész szám az adatok végéig.

Valós szám beolvasása:

read(v)
hasonló az előzőhöz: a v-vel jelölt valós változóba olvassa be a következő valós vagy egész számot. Ha van előtte szóköz vagy újsor jel, azt átlépi.

Például

var a,b,c,d,e,f,g,h:integer;
   o,p,q,r,s:char;

deklarációk esetén az

1 2
3 4 5 6 7
8 9 10 11
12 13
14 15
 v

adatok beolvasása - feltételezve, hogy minden sort az utolsó számjegy után közvetlenül a sorvége jel követ:

read(a,b,c);
readln(d,e);
read(f,g);
readln;
readln;
read(h);
read(o,p,q,r,s);
{ a=1, b=2, c=3 }
{ d=4, e=5, atlepi 6-ot es 7-et }
{ f=8, g=9 }
{ atlepi 10, 11-et, a kov. sor elejere all }
{ atlepi a 12 13 sort }
{ h=14 }
{ o=' ', p='1', q='5', r=' ', s='v' }

(Mind Turbo Pascalban, mind HiSoft-Pascalban eltérő eredményt kapunk.)

Az eoln függvény:
Ez a függvény logikai, azaz boolean típusú. Akkor ad igaz értéket, ha a beolvasás éppen egy sorvége jelhez ért. Ennek a segítségével lehet érzékelni karakterek beolvasásakor, hogy mikor jutunk a sor végére.
A sorvég érzékelése nem feltétlenül valamilyen karakternek a vizsgálatát jelenti. Ha például a tipikusan 80 oszlopos lyukkártyáról olvasunk be adatokat, akkor eoIn igaz értéke egyszerűen azt jelenti, hogy már 80 karaktert olvastunk be erről a kártyáról, tehát újabb lyukkártya beolvasása következik.

Az eof függvény:
Ez is logikai értéket ad, és értéke akkor true, ha nincs több adat, azaz a beolvasható adatok végére jutottunk. Hi-Soft Pascalban nincs implementálva.

3. További adattípusok, típusdefiníció

Az eddigiek során csak olyan változókkal foglalkoztunk, amelyek egyetlen értéket képesek tárolni. Ennyi ritkán elég feladatok megoldásához, a számítógépeket ugyanis általában éppen arra használjuk, hogy nagy mennyiségű adatot dolgozzanak fel. Ehhez pedig olyan változókra, illetve ezek deklarálásához olyan adattípusokra van szükségünk, amelyek több értéket is képesek együtt kezelni.
A Pascal nyelv több lehetőséget is biztosít ilyen adatok használatára, ezek közül most a tömböt és a rekordot mutatjuk be.

Ebben a fejezetben ismerkedünk meg egy további lehetőséggel, az adattípus-definiálással is. Ennek segítségével olyan azonosítót definiálhatunk, amely a program további részében - az előre definiált típusazonosítókhoz hasonlóan - felhasználható változók típusának megadására, vagy éppen további adattípusok definiálására.

3.1. Egyindexű tömbök
A második fejezetben már találkoztunk azzal a feladattal, hogy meg kell keresni és ki kell írni az első 100 szám közül a prímeket.
Az ott használt módszer meglehetősen lassú, különösen, ha nem csak százig vizsgáljuk a számokat, hanem például ezerig vagy tízezerig. Egy-egy számról ugyanis úgy dönti el, hogy prím-e, hogy az osztót kettőtől egyesével növeli, pedig elég lenne az addig megtalált prímeket osztónak választani. Ehhez azonban valahogyan tárolni kellene az addig megtalált prímeket, például egy ilyen táblázatban:

1. prím:
2. prím:
3. prím:
4. prím:
...
2
3
5
7
...

Tehát olyan adattípusra lenne szükségünk, amely egy számsorozat tárolására képes. Ezt az adattípust tömbnek nevezzük. Egy olyan tömbváltozó, amely 100 darab egész szám tárolására alkalmas, például így deklarálható:

var primek:array[1..100] of integer;

Ennek a tömbnek primek a neve. Az array kulcsszó jelzi, hogy tömbről van szó, elemeit 1-től 100-ig sorszámozzuk. Elemeinek típusát az of kulcsszó után kell megadni, most ez integer. A tömbelemek sorszámának értéktartományát, mint látjuk, szögletes zárójelek között, konstansokkal kell megadni Ennek a tömbnek az első elemére

primek[1]

formában tudunk hivatkozni, azaz a tömb neve után szögletes zárójelben kell a tömb elemének sorszámát megadni. Ezt a sorszámot általában "index"-nek nevezzük, mivel a matematikában a számsorozatok egyes elemeit az elemek nevének lábához írt indexszel különböztetjük meg.
Ennek megfelelően a primek tömb g-edik elemét primek [g], a 3*k+m indexű elemét pedig primek[ 3*k+m] adja meg, azaz a tömb indexe nemcsak szám, hanem változó, illetve kifejezés is lehet. Nem hivatkozhatunk azonban a primek tömb nulladik vagy kétszázadik elemére, ilyen ugyanis deklarációja szerint nincs. Ha az index értékét megadó kifejezés értéke valamilyen programozási hiba miatt mégis kilógna a megengedett indextartományból, akkor a program végrehajtása általában megszakad és hibaüzenetet kapunk.

A tömbökben olyan értékeket lehet tárolni, amilyen az elemtípusuk. Így a primek tömbben szándékunknak megfelelően egész értékeket tárolhatunk. A tömb elemeinek ugyanúgy lehet értéket adni, mint bármilyen más, eddig megismert változónak, azaz értékadó utasítással vagy beolvasással.
Megjegyezzük, hogy egyes gépeknél nincs szögletes zárójel (tehát [, ill. ]), ezért a Pascal nyelvben (de a HiSoft Pacalban nem) helyetük mindenütt használhatjuk a (. illetve .) karakterpárt is, amely minden gépen beírható, és jelentése teljesen megegyezik a szögletes zárójelek jelentésével. Így akár azt is írhatnánk, hogy primek(.g] de ez nem szép megoldás. A könyv további részében mindig a szögletes zárójelet használjuk.
A szögletes zárójel előtt és után akár a deklarációban, akár pedig a tömb indexelt hivatkozásában tetszőleges számú szóköz hagyható, de szóközök nélkül is használhatjuk a zárójeleket.

3.1.1. Példa:
Készítsünk programot az első 100 prímszám meghatározására és kiírására!

Megoldás:
Egy táblázatban tároljuk az eddig megtalált prímeket. A vizsgálandó számot (tehát amelyikről el akarjuk dönteni, hogy prím-e) csak az eddig megtalált prímekkel próbáljuk meg elosztani, hiszen ha egyik nálánál kisebb prímmel sem osztható, akkor maga is prím. Ilyenkor ezt a számot is felvesszük a táblázatba az eddig megtalált prímszámok után. Ezután a következő számmal próbálkozunk, hogy prím-e, és ezt addig folytatjuk, amíg meg nem telik a táblázatunk, azaz amíg a megfelelő számú prímet meg nem találtuk.

program primek;
const n=100;
var primek:array[1..n] of integer;
    szam,utolso,oszto,x:integer;
begin
  writeln('Az elso ',n,' primszam:');
  primek[1]:=2;write(2:6); { az elso prim }
  utolso:=1; { utolso prim indexe }
  szam:=3;   { elso vizsgalando szam }
  repeat
    x:=1;oszto:=trunc(sqrt(szam));
    while (szam mod primek[x]<>0) and (primek[x]<=oszto) do
      x:=x+1; { kovetkezo oszto }
    if szam mod primek[x]<>0 then begin { nem volt osztoja }
      utolso:=utolso+1;primek[utolso]:=szam; { uj primszam }
      write(szam:6); {prim kiirasa }
      if utolso mod 10=0 then Writeln { 10 szam egy sorban }
    end;
    szam:=szam+2 { kovetkezo vizsgalando szam }
  until utolso=n { megtelt a tablazat }
end.

A programban néhány optimalizációt elvégeztünk: ha például a 19-et vizsgáljuk, akkor azt kár elosztani 17-tel, ez már lényegesen nagyobb, mint a szám fele, és a 17-tel való osztásra csak akkor kerül sor, ha nem volt osztható 2-vel, hiszen a kicsi osztóktól haladtunk a nagyobbak felé.
Általánosabban megfogalmazva: egy szám legkisebb valódi (tehát 1-től és önmagától különböző) osztója nem lehet nagyobb, mint a szám négyzetgyöke. Ha ugyanis egy osztó ennél nagyobb, akkor a hányados, ami maga is osztója a számnak, a szám négyzetgyökénél kisebb.
A szam változót nem eggyel, hanem kettővel léptetjük, vagyis a vizsgálandó számok köréből kihagyjuk a páros számokat, hiszen a páros számok mind kettő többszörösei.

3.1.2. Példa:
Készítsünk programot, amely beolvas 1000 valós számot, kiszámítja és kiírja ezek átlagát, majd kiírja azokat a beolvasott számokat, amelyek nagyobbak az átlagnál!
Számok átlagát meghatározó programmal már találkoztunk a 2.1. fejezetben, de ha a számítás után ki akarjuk írni az átlagnál nagyobb számokat, akkor ehhez az összes szám tárolására is szükségünk van, tehát tömböt kell használnunk.

Megoldás:
A program vázlata a feladat alapján magától értetődő:

Az ennek megfelelő program:

program szamok;
const meret=20;  { a tomb merete }
var szamok:array[1..meret] of real;
    atlag:real;
    k:integer;
begin
 atlag:=0;
  for k:=1 to meret do begin
    write(k,'. szam: ');read(szamok[k]);  { beolvasas }
    atlag:=atlag+szamok[k]  { osszegzes }
  end;
  atlag:=atlag/meret; { atlag kiszamitasa }
  writeln('Atlag = ',atlag:8:2);
  writeln('Az atlagnal nagyobb szamok:');
  for k:=1 to meret do
    if szamok[k]>atlag then  { ha nagyobb az atlagnal }
      writeln(k:3,'-dik szam =',szamok[k]:8:2);
end.

Turbo Pascalban read(szamok[k]) helyett readln(szamok[k]) írandó.
Vegyük észre, hogy a tömb méretét itt a meret konstans adja meg, tehát a program könnyebben módosítható, ha más a feldolgozandó számok száma. Ez azonban még mindig nem igazán kényelmes megoldás, hiszen minden alkalommal, ha változik ez a szám, a programba bele kell javítani és újra le kell fordítani.

3.1.3. Példa
Alakítsuk át az előbbi programot úgy, hogy a feldolgozandó számok száma is bemeneti adat legyen!

Megoldás:
A tömb indexhatárait, tehát a tömb elemeinek számát a Pascal nyelvben mindig konstanssal kell megadni - legyen az akár tényleges szám, akár konstansazonosító tehát ezt az értéket már a program megírásakor ismernünk kellene. Ha ez mégsem lehetséges, akkor nem tehetünk mást, mint hogy olyan nagy tömböt deklarálunk, amekkora becslésünk szerint elég lesz a feladat megoldásához, a programban pedig nem a teljes tömböt használjuk, hanem csak az elejéről annyi elemet, amennyire éppen szükség van.
Előfordulhat azonban, hogy becslésünk rossznak bizonyul, azaz túl kicsire deklaráljuk a tömböt. A programot erre az esetre is fel kell készíteni, ilyenkor megfelelő hibaüzenet után le kell állnia.
Legyen az első bemeneti adat a feldolgozandó számok száma!

program szamok2;
const meret=1000;
var szamok:array[1..meret] of real;
    atlag:real;
    n,k:integer;
begin
  write('a feldolgozando szamok szama: ');read(n);
  if n>meret then
    writeln('tul sok, legfeljebb ',meret,' lehet!')
  else begin
    atlag:=0;
    for k:=1 to n do begin
      write(k,'. szam: ');read(szamok[k]);
      atlag:=atlag+szamok[k]
    end;
    atlag:=atlag/n;
    writeln('atlag = ',atlag:10:2);
    writeln('az atlagnal nagyobb szamok:');
    for k:=1 to n do
      if szamok[k]>atlag then
        writeln(k:3,'-dik szam =',szamok[k]:10:2)
  end
end.

Turbo Pascalban read... helyett readln... írandó.
Ez a módszer bizonyos memóriapazarlással jár, hiszen lehet, hogy az esetek nagy részében csak a tömb első néhány elemét használjuk. Tömbök esetén nincs más lehetőségünk. Létezik azonban a Pascal nyeltben olyan adattípus is, amelynek nemcsak a mérete, hanem a szerkezete is megváltoztatható a program végrehajtása során. Ezzel a lehetőséggel a dinamikus adattípusoknál találkozunk.

3.1.4. Példa:
Készítsünk programot egyetemi felvételi sorrend meghatározására: olvassa be legfeljebb 1000 jelentkező pontszámát, majd a pontszámok csökkenő sorrendjében írja ki, hogy a pontszámok alapján hányadik helyre milyen pontszámmal hányas sorszámú jelentkező került! A jelentkezők pontszámát a sorszámok sorrendjében olvassuk be, a pontszámok után 0 jelzi, hogy nincs több adat, nulla pontszámúak jelentkezését ugyanis nem fogadjuk el.

Megoldás:
A pontszámokat beolvassuk egy 1000 egészből álló tömbbe. Ezután megkeressük a legnagyobb pontszámút, kiírjuk hogy ez az első helyre került, mennyi a pontszáma és hányadik volt a listán, majd a második legnagyobb pontszámút stb.
Meg kell oldani azt is, hogy ha a legmagasabb pontszámot megtaláltuk és adatait kiírtuk, a program ezt ne vegye figyelembe többé, azaz valahogyan törölje a listáról. Egyébként ugyanis a következő menetben ismét ezt választanánk ki. Ezt úgy oldjuk meg, hogy a pontszámát -1-re változtatjuk, így az összes többi jelentkező pontszámánál kisebb lesz.
A megoldás vázlata:

pontszámok beolvasása,
helyezések ciklusa:
   legmagasabb pontszám megkeresése,
   adatainak kiírása,
   pontszámának törlése

A legmagasabb pontszámot így keressük meg: először azt feltételezzük, hogy az első jelentkező pontszáma a legnagyobb, majd végigmegyünk a többin, és ha nagyobbat találunk, akkor annak pontszámát és sorszámát jegyezzük meg, így a további elemeket már ahhoz hasonlítjuk:

legnagyobb pontszám megkeresése:
   feltesszük, hogy az első a legnagyobb pontszámú,
   ciklus a többi pontszámra:
      ha nagyobb a pontszám,
      akkor értékének és helyének megjegyzése

A program:

program felveteli1;
const max=1000;
var pontszamok:array[1..max] of integer;
  letszam,helyezes,legnagyobb,sorsz,k:integer;
begin
  { pontszamok beolvasasa }
  letszam:=0;
  repeat
    letszam:=letszam+1; {sorszam novelese }
    write(letszam:3,'. pontszam: ');read(pontszamok[letszam]);
  until (pontszamok[letszam]=0) or (letszam=max);
  if pontszamok[letszam]=0 then begin  { volt eleg hely }
    letszam:=letszam-1; {tenyleges letszam }
    { helyezesek ciklusa }
    for helyezes:=1 to letszam do begin
      legnagyobb:=pontszamok[1];sorsz:=1;
      for k:=2 to letszam do
        if pontszamok[k]>legnagyobb then begin { ez nagyobb }
            legnagyobb:=pontszamok[k];sorsz:=k
        end;
      writeln(helyezes:3,'. lett ',legnagyobb:3,
              ' ponttal a(z) ',sorsz:3,'. jelentkezo');
      pontszamok[sorsz]:=-1  { pontszam torlese}
    end
  end
  else { nincs eleg hely }
    writeln('Legfeljebb ',max-1,'jelentkezo lehet!')
end.

Turbo Pascalban read(pontszamok[letszam]) helyett readln(pontszamok[letszam]) írandó.

Stílus:
Hosszabb programok áttekinthetőségét növeli, ha az egyes nagyobb programrészeket önálló megjegyzéssorokkal választjuk el egymástól.
Segít az eligazodásban az is, ha az egymástól több sor távolságban lévő begin...end, if... then... else, illeve repeat...until kulcsszavak későbbi eleme mellé megjegyzésbe odaírjuk, mihez is tartozik.

3.1.4. Feladat:
Az előbbi program legfeljebb 999 jelentkező esetén használható. Alakítsa át a tömb méretének növelés nélkül úgy, hogy valóban 1000 pontszám feldolgozására legyen képes!

3.1.5. Feladat:
Hogyan viselkedik felveteli1 programunk egyetlen, illetve nulla jelentkező esetén?

3.1.6. Feladat:
Hogyan viselkedik programunk, ha több azonos pontszámú jelentkező is van?
Alakítsa át úgy, hogy az azonos pontszámúak azonos helyezési számot kapjanak!

Vizsgáljuk meg, hogy hány összehasonlítást kell végezni a felveteli1 programban, ha n adatot dolgozunk fel!
A helyezések száma n és mindegyikük megkereséséhez n-1 összehasonlítást végzünk, így összesen n*(n-1) összehasonlításra van szükség, ami nagy n esetén alig különbözik n^2-től. Ez kétszeres n esetén négyszeres futási időt jelent, 999 jelentkezőnél például 44 másodpercet (4.77 MHz-es IBM PC/XT gépen, 3.0 változatú Turbo Pascallal), ha a kiírás idejét nem vesszük figyelembe. Ez meglehetősen magas érték.
Hogyan lehetne gyorsítani a programot?

A program előrehaladtával egyre szaporodnak a nulla pontszámú tömbelemek, amelyekkel végül is feleslegesen végzünk műveleteket. Mennyit nyerünk, ha ezeket tényleg kihagyjuk a táblázatból?
A legmagasabb pontszám megkereséséhez először n-1 összehasonlítás kell, majd ha ezt kihagyjuk a táblázatból, a következő legnagyobb elem megkereséséhez n-2, az ezt követőhöz n-3 stb., az utolsó előttihez 1, az utolsóhoz 0. Az összehasonlítások teljes száma így:

hiszen ez egy számtani sorozat összege.
Ezzel a módszerrel tehát az összehasonlítások száma felére csökkenthető.

3.1.5. Példa:
Alakítsuk át a felveteli1 programot úgy, hogy a már kiírt pontszámokat úgy hagyja ki a táblázatból, hogy azoktól ne kelljen többet foglalkozni!

Megoldás:
A kiírt szám helyére áttesszük a táblázat utolsó elemét, a táblázat méretét, azaz a letszam változó értiekét pedig eggyel csökkentjük.
(Az itt következő programrészekben megszámoztuk az egyes utasításokat. Ezeket a sorszámokat a magyarázó ábrán is feltüntettük, hogy könnyebb legyen az utasításokat és hatásukat egymásnak megfeleltetni.)
A megváltozott programrész:

pontszamok[sorsz]:=pontszamok[letszam];
...
letszam:=letszam-1
{ 1 }

{ 2 }

Ez azonban átrendezi a táblázatot, azaz az egyes pontszámok nem azon a helyen lesznek, ahová beolvastuk őket. A jelentkezők sorszámát így ezentúl a pontszámok indexe nem tudja megadni. Olyan megoldást kell találnunk, hogy átrendezés után is meg tudjuk mondani, a tömb adott elemében tárolt pontszám hányadik volt a beolvasáskor.
Ennek a tárolására bevezetünk egy sorszamok nevű tömböt, amelyben számontartjuk, hogy az adott helyen tárolt pontszám hányadik volt a beolvasáskor. Ezt kezdetben rendre az 1...letszam értékkel töltjük fel, majd amikor egy-egy pontszámot a táblázat végéről áthelyezünk a kiírt elem helyére, ebben a tömbben is áttesszük az elem sorszámát az új helyre. Így a pontszámok beolvasáskori sorszámai nem indexük, azaz a pontszamok tömbben elfoglalt pozíciójuk adja meg, hanem az, hogy mennyi, sorszamok tömbben az ugyanilyen indexű elem értéke:

pontszamok[sorsz]:=pontszamok[letszam];
sorszamok[sorsz]:=sorszamok[letszam];
letszam:=letszam-1
{ 1 }
{ 2 }
{ 3 }

Az ennek megfelelően átalakított program:

program felveteli2;
const max=1000;
var pontszamok,sorszamok:array[1..max] of integer;
    letszam,helyezes,legnagyobb,sorsz,k:integer;
begin
  { pontszamok beolvasasa }
  letszam:=0;
  repeat
    letszam:=letszam+1;
    write(letszam:3,'. pontszam: ');read(pontszamok[letszam]);
    sorszamok[letszam]:=letszam;
  until (pontszamok[letszam]=0) or (letszam=max);
  if pontszamok[letszam]=0 then begin { volt eleg hely }
    letszam:=letszam-1; { tenyleges letszam }
    { helyezesek ciklusa }
    for helyezes:=1 to letszam do begin
      legnagyobb:=pontszamok[1];sorsz:=1;
      for k:=2 to letszam do
        if pontszamok[k]>legnagyobb then begin { ez nagyobb }
          legnagyobb:=pontszamok[k];sorsz:=k
        end;
      writeln(helyezes:3,'. lett ',legnagyobb:3,
              ' ponttal a(z) ',sorsz:3,'. jelentkezo');
      pontszamok[sorsz]:=pontszamok[letszam];
      sorszamok[sorsz]:=sorszamok[letszam];
      letszam:=letszam-1
    end
  end
  else { nincs eleg hely }
    writeln('Legfeljebb ',max-1,'jelentkezo lehet!')
end.

Turbo Pascalban read(pontszamok[letszam]) helyett readln(pontszamok[letszam]) írandó.
Ennek a változatnak valóban feleannyi idő kell, mit az előző változathoz.

3.1.6. Példa:
Oldjuk meg a felveteli1 feladatát, azaz a jelentkezők sorszámának csökkenő pontszám szerinti kiírását úgy, hogy a beolvasott adatokat először növekvő sorrendbe rendezzük, majd a pontszámok csökkenő sorrendjében kiírjuk!

Megoldás:
A program tehát három részből áll:

Az adattömbök tartalmának nagyság szerinti rendezése a programozási gyakorlatban nagyon sokszor előfordul, ezért külön is érdemes foglalkozni vele.
A feladat általános megfogalmazása így hangzik: adott egy tömb, amely valamilyen adatokat tartalmaz, és a tartalmát át kell rendezni úgy, hogy a tömbelemek a feladattól függően nagyság szerint növekvő vagy éppen csökkenő sorrendben legyenek.
Az előbbi példákban, tehát a felvételi pontszámok esetében a tömbelemek csökkenő sorrendje tűnik természetesebbnek. Nem jelent lényeges változást, ha egy tömb az értékeket növekvő sorrendben tartalmazza és nekünk csökkenő sorrendben van rá szükségünk, hiszen a tartalmát kiírathatjuk fordított sorrendben is. Például, ha a

t:array[1..m] of real;

tömb első n<=m eleme 1-től n-ig növekvő sorrendben van feltöltve, akkor elemeit a

for k:=n downto 1 do writeln(t[k])

utasítással lehet fordított sorrendben kiírni. A sorrendet meg is lehet fordítani a

for k:=1 to n div 2 do begin
   s:=t[k];
   t[k]:=t[n-k+1];t[n-k+1]:=s;
end;

programrészlettel, ahol k egész, s pedig valós változó. (Az utóbbi for ciklusban a felső ciklushatárt egy kifejezés adja meg, nem pedig konstans vagy változó, mint a legtöbb esetben, de ezt a for utasítás szabályai meg is engedik.)
Ahhoz, hogy a rendezés után is meg tudjuk adni, melyik pontszám hányadik volt a beolvasáskor, a felveteli2 programoz hasonlóan egy sorszamok segédtömböt használunk. Ennek az elemei azt adják meg, hogy, az ugyanennyiedik elem a pontszámokat tartalmazó pontszamok tömbben hányadik volt beolvasáskor.

Tömböket nagyon sokféle módon lehet rendezni. A könyv további részében ismételten foglalkozunk majd ezzel a problémával, amint az újonnan megismert nyelvi eszközök további lehetőséget adnak a rendezés módszerek hatékonyságának fokozására.
A közvetlen kiválasztásos módszer lesz az első, amivel megismerkedünk: megkeressük a tömb legkisebb elemét és felcseréljük a tömb első elemével, majd megkeressük a többi elem közül a legkisebbet, felcse-réljük a másodikkal stb. Lényegében ezt a módszert használtuk a felveteli1 és felveteli2 programokban is, de ott nem hagytuk a táblázatban a kiválasztott elemeket, hanem valamilyen módszerrel töröltük onnan.
Ez a rendezési módszer annyira egyszerű, hogy mindjárt a kész programot mutatjuk be:

program felveteli3;
const max=1000;
var pontszamok,sorszamok:array[1..max] of integer;
    letszam,legkisebb,index,k,b,s:integer;
begin
  { pontszamok beolvasasa }
  letszam:=0;
  repeat
    letszam:=letszam+1;  { sorszam novelese }
    write(letszam:4,'. pontszam: ');read(pontszamok[letszam]);
    sorszamok[letszam]:=letszam { eredeti letszam }
  until (pontszamok[letszam]=0) or (letszam=max);
  if pontszamok[letszam]=0 then begin  { volt eleg hely }
    letszam:=letszam-1;  { tenyleges letszam }

  { rendezes kozvetlen kivalasztassal }
    for k:=1 to letszam-1 do begin
      legkisebb:=pontszamok[k];index:=k;
      for b:=k+1 to letszam do
        if pontszamok[b]<legkisebb then begin  { ez kisebb }
          legkisebb:=pontszamok[b];index:=b
        end;
      pontszamok[index]:=pontszamok[k];  { pontszamok csereje }
      pontszamok[k]:=legkisebb;
      s:=sorszamok[k];  { sorszamok csereje }
      sorszamok[k]:=sorszamok[index];
      sorszamok[index]:=s;
    end;

  { kiiras csokkeno sorrendben }
    for k:=1 to letszam do
      writeln(k:4,'. lett ',pontszamok[letszam+1-k]:3,
        ' ponttal a(z) ',sorszamok[letszam+1-k]:4,'. jelentkezo')
  end
  else { nincs eleg hely }
    writeln('legfeljebb ',max-1,'jelentkezo lehet!')
end.

Turbo Pascalban read(pontszamok[letszam]) helyett readln(pontszamok[letszam]) írandó.

A programban k a külső, b a belső for ciklus ciklusváltozója, azaz k adja meg, hogy hányadik legkisebb elemet keressük, b pedig azt, hogy melyik elemet vizsgáljuk, kisebb-e, mint az eddigi legkisebb.
Előfordulhat az is, hogy valóban éppen a k-adik elem a legkisebb. Ilyenkor nem is lenne szükség a cserére, hiszen a k-adik elemet saját magával csévéljük fel. Ezt megtakaríthatnánk, ha a csere elé beiktatnánk egy vizsgálatot:

if index<>k then {tényleg cserélni kell}
begin
   pontszamok[index]:= ...
end;

Nagyon kicsi a valószínűsége, hogy ez a feltétel ne teljesüljön, a feltételvizsgálatot azonban minden tömbelemre elvégezzük, és ez lassítja a programot. A két hatás végül kiegyenlíti egymást, a futási idő nem változik.

Megtehettük volna azt is, hogy a legkisebb elemnek csak az indexét jegyezzük meg b ciklusában, az értékét nem. Ekkor a következő pontszám vizsgálatát így kellett volna elvégezni:

...
if pontszamok[b]<pontszamok[index] then index:=b;
...

tehát az összehasonlítás jobb oldalán a legkisebb nevű önálló változó helyett egy tömb indexes eleme szerepelne.
A tömbök elemeihez való hozzáférés azonban mindig bonyolultabb, tehát több időt igényel, mint önálló változók elérése, ez tehát lassította volna a programot (Turbo Pascalban közel 30 százalékkal).

3.2. Többindexű tömbök

A felveteli3 programban két tömb elemeit párhuzamosan használtuk, azaz ha megváltozott az egyik tömbben egy elem, ennek megfelelően meg kellett változtatni (például fel kellett cserélni) a másik tömb megfelelő elemeit is. Emiatt érdemesebb lenne az azonos indexű pontszámot és sorszámot együtt kezelni. Erre ad lehetőséget, hogy tömbnek nem csak egy, hanem kettő, három vagy akár öt, sőt elvileg tetszőleges számú indexe is lehet.
Például, ha egy t tömböt így deklarálunk:

t:array[1..maxletszam] of array[1..2] of integer;

Akkor ez olyan adatszerkezetet jelent, amely maxletszam elemből áll és minden eleme 2 egészet tartalmazó tömb. Ezt deklarálhattuk volna így is, a két változat egyenértékű:

t:array[1..maxletszam,1..2] of integer;

Ezt a kétindexű tömböt úgy ábrázolhatjuk, ahogy a jobb oldali képen láthatjuk.
Ez maxletszam sorból és soronként 2 elemből áll.

A t tömb elemeire szintén index segítségével hivatkozhatunk. A következő jobb oldali ábra ezt mutatja be, a hivatkozott tömbelemet vastag vonal határolja.
Amint látható, az első index azt adja meg, hogy melyik sorról, a második pedig azt, hogy melyik oszlopról van szó. Az, hogy az első indexnek a sorokat, a másodiknak pedig az oszlopokat feleltetjük meg, teljesei önkényes hozzárendelés, hiszen akár az első index is jelenthetné az oszlopokat. Ez a megfeleltetés azonban ugyanolyan, mint ahogyan a matematikában a mátrixok indexeit megadják.

Ha csak egyetlen indexet adunk meg, az az első indexet jelenti, tehát ez a tömbhivatkozás mindazokra a tömbelemekre, amelyeknek ilyen az első indexük. Egy index esetén a tömb teljes sorára, esetünkben az adott sor mindkét elemére hivatkozunk, ez tehát egy kételemű tömböt jelent. Ennek adott eleméhez szintén index használatával tudunk hozzáférni, ezt a második indexet megadhatjuk újabb indexzárójelben:

t[3][2]

de megadhatjuk a két indexet egyetlen szögletes zárójelpárban, vesszővel elválasztva is:

t [3,2]

3.2.1. Példa:
Alakítsuk át a felvételi3 programot úgy, hogy egyetlen, de kétindexű tömböt használjunk! Ennek első oszlopa jelentse a pontszámot, a második pedig az eredeti sorszámot!

program felveteli4;
const max=1000;
var t:array[1..max,1..2] of integer;
    letszam,legkisebb,index,k,b,s:integer;
begin

  { pontszamok beolvasasa }
  letszam:=0;
  repeat
    letszam:=letszam+1;  { sorszam novelese }
    write(letszam:4,'. pontszam: ');read(t[letszam,1]);
    t[letszam,2]:=letszam    { eredeti letszam }
  until (t[letszam,1]=0) or (letszam=max);
  if letszam<max then begin  { volt eleg hely }
    letszam:=letszam-1;  { tenyleges letszam }

  { rendezes kozvetlen kivalasztassal }
    for k:=1 to letszam-1 do begin
      legkisebb:=t[k,1];index:=k;
      for b:=k+1 to letszam do
        if t[b,1]<legkisebb then begin  { ez kisebb }
          legkisebb:=t[b,1];index:=b
        end;
      t[index,1]:=t[k,1];  { pontszamok csereje }
      t[k,1]:=legkisebb;
      s:=t[k,2];  { sorszamok csereje }
      t[k,2]:=t[index,2];
      t[index,2]:=s;
    end;

  { kiiras csokkeno sorrendben }
    for k:=1 to letszam do
      writeln(k:4,'. lett ',t[letszam+1-k,1]:3,
        ' ponttal a(z) ',t[letszam+1-k,2]:4,'. jelentkezo')
  end
  else { nincs eleg hely }
    writeln('Legfeljebb ',max-1,'jelentkezo lehet!')
end.

Ahhoz, hogy kialakíthassuk a program végleges változatát, szükségünk van a tömbökre vonatkozó szabályok, valamint a típusdefiniálás megismerésére.
Amikor a kétindexű tömbbel megismerkedtünk, láttuk, hogy az olyan egyindexű tömbnek is tekinthető, amelynek az elemei szintén egyindexű tömbök. Ennek analógiájára lehet háromindexű tömböt is deklarálni, amelynek elemei kétindexűek stb.

Háromindexű tömböket például így deklarálhatunk:

var
   dd,er:array[1..12,-2..5,10..19] of char;
   fg:array [3..5] of array [1..100] of array [0..11] of real;
   s,s2:array[1..3,7..22] of array [1..2] of integer;

Az indexhatárok megadását tehát itt is összevonhatjuk egyetlen indexzárójelbe, vagy megadhatjuk tetszőleges csoportosításban külön-külön is.

Az s tömb szerkezetét a jobb oldali ábrán szemléltethetjük.
Az begyindexű tömböket tehát úgy tekinthetjük, mint elemeik egyetlen sorozatát, a kétindexűeket úgy, mint elemeik vízszintes és függőleges, azaz kétdimenziós sorozatát, a háromindexűeket, mint háromdimenziós elemsorozatot, ezért a k indexű tömböt szokás k dimenziósnak is nevezni.
s[3,7,2] az előző példában egy egész számot jelent, az s tömbnek ezt az elemét a következő formák bármelyikével megadhatjuk:

s[3][7][2]
s[3][7,2]
s[3,7][2]

Hogy ezek közül melyiket használjuk, az csak attól függ, melyiket találjuk áttekinthetőbbnek, jobban olvashatónak, és teljesen független attól, hogy a tömb deklarációjakor milyen csoportosításban adtuk meg az indexhatárokat.
s[1,9] kettő egész számból áll, s [2] (22-7+1)*2=32 darabból, a teljes s tömb pedig 3*32=96 darabból.

Az előbbi deklarációkban újdonságot jelentett az is, hogy az alsó indexhatár nem mindig 1, ennél nagyobb, sőt negatív érték is előfordulhat. Ez megengedett és a választás megint csak a feladat jellegéből következik. Ha például egy kívül 5 emeletes, két alagsori szinttel rendelkező emeletes garázs egyes szintjein lévő autók számát akarjuk egy tömbben tárolni, akkor erre egy

array[-2..5] of integer

típusú tömb a legmegfelelőbb. Ebben a 0. elem a földszintre vonatkozik, a 2. a második emeletre, a -1 indexű pedig közvetlenül a földszint alatt levőre.
A tömb alsó indexhatárának kisebbnek vagy egyenlőnek kell lennie a felsővel. (Az egyenlőségnek nem sok értelme van, hiszen ez azt jelenti, hogy az index egyetlen értéket vehet csak fel.)

Mivel a mi fizikai világunk háromdimenziós, nehezen tudjuk elképzelni a 4, 5 vagy esetleg 11 dimenziós tömböket, mégis van mód ábrázolásukra. A következő ábra a négydimenziós tömb lehetséges értelmezését mutatja:


t4:array[0..2,1..3,1..4,1..3]

Ötdimenziós tömb szerkezete:


t5:array[1..2,0..2,1..3,1..4,1..3]

3.2.2. Példa:
Definiáljunk tömböt, amely alkalmas egy gimnáziumban az év végén kiosztott bizonyítványok jegyeinek támlására!

Megoldás:
Az adatok tárolásához négydimenziós tömbre van szükség. Ennek első indexe megadja, hogy hányadik évfolyamról, a második, hogy az adott évfolyamban melyik osztályról, a harmadik, hogy az osztály hánya dik tanulójáról, a negyedik pedig, hogy a tanuló hányadik osztályzatáról van szó. A tömb méreteit konstansazonosítókkal adjuk meg, hogy szükség esetén könnyen lehessen módosítani:

const
  evszam=4;   { evfolyamok szama }
  osztszam=3; { osztalyok szama egy evben }
  letszam=40; { tanulok szama egy osztalyban }
  tantargysz=13; { tantargyak szama }
  ...
var
  jegyek:array[1..evszam,1..osztszam,1..letszam,1..tantargysz]
         of integer;

Ahhoz, hogy többdimenziós tömbökkel hatékonyan készíthessünk programokat, szükségünk van az adattípusok definiálására is, ezért ilyen algoritmusokkal a típusdefiniálás bemutatása után foglalkozunk.


A tömb- (array) típus

1. A tömb azonos típusú adatok sorozata, elemeit az elemek indexének megadásával érjük el, minden tömbelem önálló értéket képvisel.

Nem szabványos típus, azaz minden tömböt deklarálnunk kell a programban.

A tömbelemek száma a tömb definíciója által rögzített, azaz a program végrehajtása során nem változik.

2. A tömbtípus megadásának formai szabályai:

ahol az indextípus: sorszámozott típus,

elemtípus: típus.

Példák:

array [1..100] of real
array [boolean,'A'..'Z'] of mutato
array [-3..5] of record
   a,b:real;
   c:char
end

3. A tömbtípus minden ilyen alakú megadása az összes előző tömbtől különböző, új tömbtípust vezet be, akár típusdefinícióban, akár változó deklarációjában használjuk.
Így például

var a:array [1..10] of char;
     b:array [1..10] of char;

esetén a és b típusa nem azonos, nem is kompatibilis. Ha azt akarjuk, hogy azonos típusúak legyenek, akkor deklaráljuk őket közös változólistán:

var a,b:array [1..10] of char;

vagy definiáljunk megfelelő típusazonosítót és használjuk azt a és b deklarációjában:

type tt=array [1..10] of char;
var a:tt;
     b:tt;

4. Ha a tömbtípus megadásában kettő vagy több indextípust sorolunk fel, az rövidített megadása annak az ekvivalens tömbtípusnak, amelynek egyetlen indextípusa azonos a rövidített megadás indextípus-listájából az első elemmel, elemtípusa pedig egy olyan, eggyel kisebb indexű tömbtípus, amelynek az indexlistája a rövidített megadás indextípus-listájából az első indextípus nélküli rész, az elemtípusa pedig annak elemtípusával azonos.
A következők azonos tömbtípust határoznak meg:

array [boolean] of array [1..10] of array [meret] of real
array [boolean] of array [1..10,meret] of real
array [boolean,1..10,meret] of real
array [boolean,1..10] of array [meret] of real

Pakolt tömbtípusok esetén azonosak:

packed array [1..10,1.. 8] of boolean

és

packed array [1..10] of packed array [1..8] of boolean

Ezekkel nem ekvivalens az alábbi, hiszen a külső indextípus szerinti elemtípus nem pakolt:

packed array [1..10] of array [1..8] of boolean

5. Egy n elemű tömb értékét az elemek értékeinek összessége adja, így ha egy-egy elem k különböző értéket vehet fel, akkor a teljes tömb értékkészlete k^n különböző értékből áll. Ha a tömb bármelyik elemének értéke meghatározatlan - például azért, mert még nem kapott értéket -, vagy valamilyen okból értéke meghatározatlanná vált, akkor a tömb egészének értéke is meghatározatlan. (b)
Ha egy tömb minden eleme meghatározatlan, akkora tömböt "teljesen meghatározatlan"-nak nevezzük és ez az egyszerű változókra használt "meghatározatlan" kifejezés szinonimája.

A tömbváltozó olyan változóhivatkozás, amely tömb típusú változót ad meg. (Ez lehet önálló tömbváltozó vagy összetett változó valamelyik tömb típusú eleme.)

6. Teljes tömbbel a következő műveletek végezhetők:

7. Tömb elemére a tömb és az index megadásával hivatkozhatunk.
A hivatkozott tömbelemet indexelt változónak nevezzük.
Formai szabályok:

indexelt változó:

ahol tömbváltozó: változóhivatkozás
indexkifejezés: kifejezés.

Az indexkifejezésnek értékadás-kompatibilisnek keli lennie a megfelelő tömbindex típusával, így értékének az indextípus által meghatározott tartományban kell lennie.

Az indexelt változó a tömbnek annyiadik eleme, ahányadikat az indexkifejezés a tömbindex típusa által meghatározott elemsorozatában kijelöl.

Az indexelt változó típusát a tömb elemtípusa adja meg.
Például:

t:array [-5.. 15] of real;
t[3]
t[5*j-k]
deklaráció esetén
és
is real típusú értéket jelent.

8. Ha a tömbváltozó maga is indexeit változó, az egymást követő indexeket rövidített alakban is megadhatjuk: az indexek közötti ] [ zárójelpár helyett vesszőt is használhatunk, az eredetién rövidített alak azonos jelentésű.
Például:

m[k][g]

és

m[k,g]

ekvivalens.

Az indexek kiszámításának sorrendje implementációfüggő. (c)

9. Az indexelt változóval, azaz a tömbelemmel mindazok a műveletek elvégezhetők, amelyeket annak típusa megenged, kivéve, hogy tömbelem nem lehet for utasítás ciklusváltozója. (d)

Magyarázatok, megjegyzések:

(a) Mivel néhány gép billentyűzetén nincs [ és ] billentyű, a [ helyett használható a (. karakterpár, a ] helyett pedig a .) karakterpár is. (A HiSoft-Pascal nem ismeri ezen helyetteítő-karakterpárokat.)

(b) A tömb egy eleme vagy úgy válhat határozatlanná, hogy dispose eljárás első paramétereként használjuk; vagy a tömbelem változó résszel rendelkező rekordot is tartalmaz, amelyben megváltoztatjuk a változatkijelölő mező értékét; vagy értéket adunk az eddig használt változat eltérő változata valamelyik mezőjének, és a változó rész ezen újonnan használt változata további mezőt is tartalmaz.
(A dispose eljárásról és a rekord adattípusról a későbbiekben lesz szó, itt csak a teljesség kedvéért említettük ezeket az eseteket.)

(c) A megvalósítástól függ, hogy a gép pl. az m[k,g] hivatkozásban először az első index - itt: k -, majd utána a második index - itt: g - értékét határozza-e meg vagy fordítva. Ha k és g változó, ennek a szabálynak nincs jelentősége, de ha k és g függvény vagy függvény-hivatkozást is tartalmazó kifejezés, akkor a két index kiszámításának sorrendje befolyásolhatja az indexek értékét.

(d) Emlékeztetjük az Olvasót arra, hogy for utasítás ciklusváltozója csak önálló változó lehet.


3.3. Típus definiálása: type

A felveteli4 programban nem használtunk ki egy olyan lehetőséget, amit az egyébként sejtetett: a t tömb egy-egy egész sorára is hivatkozhatnánk, amikor fel kell cserélni a tömb k-adik és index-edik elemét. Vagyis nem lenne szükség arra, hogy azokat az elemeket, amelyeknek a második indexe 1, illetve 2, külön-külön cseréljük fel. Ehhez olyan segédváltozóra van szükségünk, amelynek típusa megegyezik a t tömb egy sorának típusával, azaz

array [1..2] of integer

típusú. Az adattípus fogalmával már eddig is találkoztunk, de a tömb megismeréséig csupa előre definiált típusazonosítót láttunk, hiszen ilyen az integer, real, boolean és char is.
A tömbtípus ezektől eltért, hiszen az nem szabványos típus, hanem a programban nekünk kellett megadni.
A Pascal nyelvben definiálhatunk olyan azonosítót is, amely adattípust jelent. Ezt a típusdefiníciós részben tehetjük meg, amely a konstansdefiníciós rész és a változódeklarációs rész között van:

const ... { konstansdefiníciós rész }
type  ... { típusdefiníciós rész }
var    ... { változódeklarációs rész }
begin
   ... {utasításrész}
end.

Definiáljunk a t tömb egy sorának megfelelő adattípust és alakítsuk át a felveteli4 programot ennek a felhasználásával!

program felveteli4a;
const max=1000;
type sor=array[1..2] of integer;
var t:array[1..max] of sor;
    letszam,legkisebb,index,k,b,s:integer;
    ssor:sor;  { segedvaltozo a cserehez }
begin

  { pontszamok beolvasasa }
  letszam:=0;
  repeat
    letszam:=letszam+1;  { sorszam novelese }
    write(letszam:4,'. pontszam: ');read(t[letszam,1]);
    t[letszam,2]:=letszam    { eredeti letszam }
  until (t[letszam,1]=0) or (letszam=max);
  if letszam<max then begin  { volt eleg hely }
    letszam:=letszam-1;  { tenyleges letszam }

  { rendezes kozvetlen kivalasztassal }
    for k:=1 to letszam-1 do begin
      legkisebb:=t[k,1];index:=k;
      for b:=k+1 to letszam do
        if t[b,1]<legkisebb then begin  { ez kisebb }
          legkisebb:=t[b,1];index:=b
        end;
      ssor:=t[k];  { sorok cseréje }
      t[k]:=t[index];
      t[index]:=ssor;
    end;

  { kiiras csokkeno sorrendben }
    for k:=1 to letszam do
      writeln(k:4,'. lett ',t[letszam+1-k,1]:3,
        ' ponttal a(z) ',t[letszam+1-k,2]:4,'. jelentkezo')
  end
  else { nincs eleg hely }
    writeln('Legfeljebb ',max-1,'jelentkezo lehet!')
end.

A sor azonosító most nem változót és nem is konstanst jelent, tehát nem tartozik hozzá konkrét élték a program végrehajtásának egyetlen pillanatában sem. Adattípust, mégpedig egy kételemű tömb típusát jelenti. Az így definiált azonosítót ugyanúgy használhatjuk változók vagy esetleg további típusok definiálására, mint az előre definiált típusazonosítókat. A felveteli4a programban például a sor típusnevet az ssor változó és a t tömb elemtípusának megadására használtuk.
Mivel t[k] a tömb egyetlen sorát jelenti, ez sor típusú, tehát típusa azonos a cseréhez használ ssor segédváltozó típusával. Azonos típusú adatokat egyetlen értékadás segítségével átmásolhatni egymásba, akármilyen összetett legyen is típusuk (kivéve, ha van file típusú elemük). Így programunkban a cseréhez használt értékadások valóban elvégezhetők.
(A pontosság kedvéért meg kell jegyezni, hogy az előbbi szabály az értékadásról nem teljesen igaz: nem használható az értékadás olyan változók között, amelyek adatállomány (file) típusúak vagy van adatállomány típusú elemük.)

Az a lehetőség, hogy adattípust is lehet definiálni, a Pascal nyelv egyik legkellemesebb tulajdonsága, és ezt a programok tervezésekor gyakran ki is használjuk. Segítségével a programban használt adatszerkezetek leírására igényeinknek megfelelő típusokat tudunk definiálni és az így bevezetett típusazonosítókat jól fel tudjuk használni változók vagy éppen további típusok megadására.
A felveteli4a programban például a t tömb típusát, majd ezzel magát a tömböt is definiálhatnák a következő módon:

type
   sor=array [1..2] of integer;
   tt=array [1..max] of sor; {t tipusa}
var t:tt;

t ilyen definíciója teljesen azonos a programban bemutatottal, de most szétválasztottuk t típusának és magának a t változónak a megadását.

Fontos, hogy itt is betartsuk azt a szabályt, hogy egy újabb típus definiálásakor csak olyan típusazonosítóra hivatkozzunk, amelyet előzőleg már definiáltunk, vagy pedig standard, azaz előre definiált típus. (Egyetlen kivétel lesz ezen szabály alól, azt majd a mutatótípusnál fogjuk megismerni.)
Az előre definiált típus - mint arról már esett szó - úgy tekinthető, mintha definíciója a program szövege előtt lenne, azaz mintha megelőzné minden, a programunkba írt definíciónkat és deklarációnkat.
Az adattípusok tetszőleges mélységben hivatkozhatnak egymásra, vagyis egy típus definiálásakor hivatkozhatunk egy előző típusnévre, amelyben szintén hivatkozhatunk egy előzőre stb. Az olyan programokban, ahol összetett típusokra van szükségünk, megengedett, sőt természetes is a többlépcsős típusmegadás.
A következő típusdefiníció-sorozat egy gimnázium összes tanulójának bizonyítványát adja meg:

const
   evszam=4; { evfolyamok szama }
   maxoszt='C'; { utolso osztaly betujele egy evben }
   maxletsz=35; { tanulok max. szama egy osztalyban }
   maxtantsz=13; { tantargyaqk max. szama }
type
   bizonyitvany=array[1..maxtantsz] of integer;
      { egy tanulo bizonyitvanya }
   osztaly=array[1..maxletsz] of bizonyitvany;
      { egy osztaly bizonyitvanyai }
   ev=array['A'..maxoszt] of osztaly;
      { egy evfolyam adatai }
   gimi=array[1..evszam] of ev;
      { a teljes gimnazium adatai }

A listán négy adattípust definiáltunk, mindegyik valamilyen tömb típusát adja meg, és az első kivételével mindegyik hivatkozik az őt megelőző típusra.
A bizonyitvany azonosító egy olyan tömb típusát jelenti, amely a tantárgyak legnagyobb lehetséges számának megfelelően 13 egész számból áll.
Egy osztály bizonyítványainak sorozatát, azaz egy egész osztály összes bizonyítványának jegyeit az osztaly típusú tömbben tárolhatjuk. Ez tehát kétindexű, vagy más szóval kétdimenziós tömb. Ha deklarálunk egy

haromb:osztaly;

változót, akkor ebben a 6. tanuló bizonyítványát haromb[6] adja meg, ez bizonyitvany típusú, ebben a 3. jegyre például haromb[6,3] alakban hivatkozhatunk.
Ha az iskolában három párhuzamos osztály van, és ezeket az A, B, C betűkkel jelöljük, akkor célszerű az egyes osztályokat ennek a betűjelzésnek megfelelően indexelni. Erre a Pascal nyelv lehetőséget ad, hiszen tömb indextípusa bármilyen sorszámozott típus lehet, a karaktertípus pedig sorszámozott, hiszen a karakternek van ord értéke.
Az ev típus definíciója maxoszt értékének megfelelően egyenértékű az

ev=array['A'..'C'] of osztaly;

definícióval, tehát az index értéke 'A' karaktertől 'C' karakterig mehet. Ha feltételezzük, hogy ebben a tartományban valóban csak nagybetűk vannak, akkor ez háromelemű tömböt jelent, amelynek az elemei osztaly típusúak. Ha deklarálunk egy

ph:ev;

változót, akkor ez három párhuzamos osztály adatait képes tárolni. A C osztály adatait ph['C'] adja meg, ebben a 22. tanuló bizonyítványára például ph['C',22] alakban hivatkozhatunk, ennek 10. jegyére pedig ph['C',22,10] formában.
A teljes gimnázium 4 évfolyamból áll, tehát

Revay:gimi;

adja meg a Révay Gimnázium összes bizonyítványának adatait. Ez már egy négyindexű, azaz négydimenziós tömb. Ebben a 4. B osztályra Revay[4,'B'] alakban, ebben az osztályban a 15. tanuló 3. jegyére pedig a következő módok bármelyikével hivatkozhatunk (a gyakorlás kedvéért adunk meg több alakot):

Revay[4]['B'][15][3]
Revay[4]['B'][15,3]
Revay[4,'B',15,3]
Revay[4]['B',15,3]
Revay[4,'B',15][3]
Revay[4,'B'][15,3]

Ezekben a tömbelem-hivatkozásokban az indexeket mindig konstansok adták meg, de bármelyik index értékének kijelölésére használhatunk változót vagy kifejezést is. A következő négyszeresen egymásba ágyazott ciklus segítségével oldhatjuk meg a teljes Revay tömb adatainak beolvasását, feltételezve a

var ev,tanulo,tantargy:integer;
     oszt:char;

deklarációkat:

for ev:=1 to evszam do   {evek}
  for oszt:='A' to maxoszt do   {parhuzamos osztalyok}
    for tanulo:=1 to maxletsz do   {tanulok az osztalyban}
      for tantargy:=1 to maxtantsz do   {tantargyak}
        read(Revay[ev,oszt,tanulo,tantargy]);

Az előző példa a többdimenziós tömb bevezetésére szolgált, de tudnunk kell, hogy akár az egydimenziós, akár a többdimenziós tömböknél csak ritkán használjuk a tömbök minden elemét, azaz csak ritkán töltjük fel a tömböt a deklarált méretig. Egy osztály létszáma például általában kisebb, mint 35. Ilyenkor tipikusan a tömb első elemeit használjuk, és külön változóban tároljuk azt, hogy mekkora ez a használt hossz. Természetesen a használt hossz legfeljebb a deklarált hossz lehet.

3.3.1. Feladat:
Tervezzen adattípusokat, melyek lehetővé teszik gimnáziumi bizonyítványok adatainak a tényleges létszámok, tantárgyszámok, stb. szerinti feltöltését!

3.3.2. Feladat:
Milyen sorrendben kell megadni az adatokat az iménti beolvasáshoz?

3.3.3. Feladat:
a) Miben különbözik a tárgyalt definíció a bizonyítványok valódi szerkezetétől?
b) Alakítsa át a fejezetben tárgyalt definíciókat úgy, hogy a bizonyítványok valódi szerkezetének feleljenek meg!
c) Készítsen programrészletet az így kapott adatszerkezet elemeinek beolvasására! (Vigyázat: a másodikosoknak még nincsenek harmadikos és negyedikes jegyeik!)
d) Adott egy b) feladat szerinti tömb az előző évi bizonyítványosztás szerint kitöltve. Készítsen programrészletet, amely elvégzi az aktuális évi bizonyítványosztásnak megfelelő adminisztrációt: megváltoztatja a jegyek helyét annak megfelelően, hogy minden tanuló eggyel magasabb osztályba került, majd csak az új jegyeket olvassa be. (Feltételezzük, hogy senki nem bukott meg, nem ment el, és új tanulók sem jöttek.)


Adattípusok definiálása

1. Az adattípus-definiálás olyan azonosítót vezet be, amely a definícióban megadott adattípust képviseli.

2. Alakja:

típusdefiníció:

azonosító = típusmegadás

Ahol a típusmegadás: típusazonosító vagy új típus.

Az új típus megadása azt jelenti, hogy nem egy már definiált típusazonosítóra hivatkozunk, hanem az egyes konkrét adattípusoknál (pl. intervallum, tömb, rekord) megadott módon definiálunk új típust.
Például

tt=array[1..100] of real;

új tömbtípust definiál, melynek azonosítója tt és egy indexe van, amely 1-től 100-ig vehet fel értékeket. A tömb tehát 100 elemű, elemei pedig valós típusúak.

3. Új típus definiálása - azaz, amikor a definícióban nem típusazonosítóra hivatkozunk- az összes előzőtől különböző típus definiálását jelenti. Például a

t1=array[1..10] of real

és a

t2=array [1..10] of real

definíciók nem azonos típust definiálnak, és mivel t1 és t2 nem egyszerű típusok, nem is kompatibilisek, bár alakjuk azonos, t1 és t2 akkor lenne azonos típus, ha pl. t2 definíciója t2=t1 vagy t1 definíciója t1=t2 lenne.

4. Egy adattípus azonosítójára való minden hivatkozás ugyanazt a típust jelenti, mint amit annak definíciója megad.

5. A típusdefinícióban a típusmegadás nem hivatkozhat az éppen definiálandó típusazonosítóra. Hibás például:

r=record
  a,b:real;
  kov;^r; { Hibas! }
end;

5. Az adattípusokat három csoportba soroljuk:

 


Az adattípusok definiálására vonatkozó 3. szabály azt is jelenti, hogy például az

a: array[0..5] of char;
b: array[0..5] of char;

definíció esetén az a és b változók típusa különböző lesz.
Ha mégis azonos típusúra akarjuk ezeket deklarálni, akkor két lehetőségünk is van:

Az azonos jellegű típusok ilyen - első pillanatra túl szigorúnak tűnő - megkülönböztetését az teszi indokolttá, hogy ha a programban azonos típusú összetett adatszerkezeteket akarunk használni, még véletlenül se fordulhasson elő, hogy amikor az egyiknek megváltoztatjuk a típusát, akkor a másikéról elfeledkezünk.
Ez tette szükségessé a felveteli4a programban bemutatott megoldáshoz is típusdefiníció bevezetését, hiszen a

var t:array[1..max] of sor;
    ssor:array[1..2 of integer]; { segedvaltozo a cserehez }

deklarációk esetén például az

ssor:=t[k];

utasítás hibás lenne, hiszen t egyindexű elemtípusa különbözik ssor típusától, még akkor is, ha az ugyanolyan alakú.

3.4. Tömbalgoritmusok

Ebben a fejezetben egyszerű tömbalgoritmusokkal foglalkozunk. Ismertetünk még néhány fontos, tömbökkel kapcsolatos módszert a 3. fejezet további részében és az eljárások, illetve függvények tárgyalásakor is.

Hosszú egész számok tárolása
Az egész számok legnagyobb lehetséges értékére minden programozási nyelvben létezik valamilyen korlát, ezt Pascalban maxint adja meg. Ha ennél nagyobb - esetleg lényegesen nagyobb - egész értékeket akarunk teljes pontossággal tárolni, akkor nem elég egyetlen integer típusú adat, hanem célszerű egész számok egy sorozatát használni.
Például a

const hossz=100;
type hosszu1=array [1..hossz] of integer;
var n1,n2,e:hosszu1;

definíciók és deklarációk esetén az n1 változót úgy tekinthetjük, mint egy 100 jegyű számot, amelynek minden jegye 0 és maxint közötti értéket vehet fel, tehát értékkészlete 16 bites gépeknél, ahol maxint=32767:

0...maxint^100

vagyis körülberül 0...10^451
Ez meglehetősen nagy szám, ha figyelembe vesszük, hogy becslések szerint a világmindenséget alkotó elemi részek száma 10^47, vagy hogy a "nagy robbanás" óta csak 4,7*10^17 másodperc telt el.
Nem érdemes azonban a tömb elemeiben egészen maxint-ig terjedő értékeket tárolni, mert már két ilyen szám összeadása is gondokat okoz, a szorzásról nem is szólva. Ezért a hosszú számok egy-egy számjegyére csak 0.99 értéket engedünk meg, ilyen számjegyekkel már kényelmesen végezhetünk műveleteket, a legnagyobb ábrázolható érték pedig még mindig elég nagy: 100^100-1

3.4.1. Példa:
Készítsünk programot két hosszu1 típusú szám összeadására; a számban az 1 indexű elem a legnagyobb, a hossz indexű pedig a legkisebb helyiértékű, egy-egy helyiértéken 0 és 99 közötti számot tárolunk.

Megoldás:
Az összeadást úgy végezzük, mint ahogyan papír-ceruza módszerrel szoktuk: a legkisebb helyiértéktől indulva összeadjuk az azonos helyiértékű számjegyeket. Ha egy adott helyen a legnagyobb megengedett számjegynél nagyobb keletkezik, akkor lesz átvitel is; az adott helyiértékű eredmény pedig az összegből kivonva a számrendszer alapszáma (ez most 100).
Az előbbieken kívül deklaráljuk még az

osszeg,atvitel,index:integer;

segédváltozókat, ezekkel az e:=n1+n2 műveletet a következő programrészlet végzi el:

atvitel:=0;
for index:=hossz downto 1 do begin
  osszeg:=n1[index]+n2[index]+atvitel;
  if osszeg>=100 then begin
    atvitel:=1;osszeg:=osszeg-100;
  end
  else atvitel:=0; e[index]:=osszeg;
end;
if atvitel<>0 then writeln ('Túlcsordulás!');

Hibaüzenetet akkor kapunk, ha a legmagasabb helyiértéken is keletkezett átvitel, tehát az eredmény nem fér el e-ben.
Nagyon gyakori, hogy az ilyen nagy számok első jegyei nullák, ezekkel teljesen felesleges műveleteket végezni. Módosítsuk a számábrázolás módját úgy, hogy az index elvileg nullától kezdődjön, de a nulladik elemben ne egy számjegyet tároljunk, hanem az értékes számjegyek számát, azaz a tömb feltöltött részének hosszát:

type hosszu2=array [0..hossz] of integer;
var n1,n2,e:hosszu2;

3.4.2. Feladat:
Találjon ki megfelelő adattípust előjeles hosszú egész számok tárolására! Készítsen programrészleteket a négy alapművelet elvégzésére!

3.4.3. Feladat:
Készítsen programrészletet hosszú előjeles egész számok beolvasására, illetve kiírására!

3.4.4. Feladat:
Készítsen programot, amely kiszámítja és kiírja az utolsó adatig beolvasott számok faktoriálisának pontos értékét, a számok akár 10 jegyű decimális értékek is lehetnek!

Lottószámok sorsolása

3.4.2. Példa:
Készítsünk programot, amely tippet ad egy lottószelvény kitöltéséhez, azaz véletlenszám-generátor segítségével kisorsol és kiír 5 egész számot 1 és 90 között!

Megoldás:
A feladat első ránézésre furcsának tűnhet, hiszen eddig éppen azzal foglalkoztunk, hogy olyan algoritmusokat készítsünk, amelyek adott bemeneti adatok esetén mindig biztosan ugyanazt az eredményt számolják ki, most pedig valamilyen véletlenszerű eredményt akarunk.
A véletlenszámokat felhasználó algoritmusok nagyon fontosak a számítástechnikában. Segítségükkel sok olyan probléma hatékony kezelésére van mód, amelyet más módszerekkel csak nagyon nehézkesen tudnánk megoldani. A véletlenszámokat felhasználó módszereket szokás "Monte-Carlo" módszereknek is nevezni. [11]

A feladat megoldásához először is szükségünk van valamilyen módszerre, amellyel lehetőleg egyenletes eloszlású véletlenszámokat tudunk előállítani valamilyen számtartományban. Erre a legtöbb Pascal-megvalósításban létezik előre deklarált függvény. A Turbo Pascalban például random a neve, és kétféle módon is használható: ha megadunk egy pozitív egész paramétert, akkor 0 és a paraméter értéke közötti egyenletes eloszlású álvéletlen egész értékeket ad, ha pedig nem adunk meg paramétert, akkor 0 és 1 közötti valós típusú, egyenletes eloszlású álvéletlen értékeket.
Azt a véletlen értéket előállító módszert, ami tipikusan eljárás vagy függvény formájában áll rendelkezésünkre, véletlenszám-generátornak is szokás nevezni. Úgy használjuk, hogy minden egyes alkalommal, mikor szükségünk van egy újabb véletlen értékre, meghívjuk a függvényt vagy eljárást, és az megadja a következő álvéletlen számot.
Az "álvéletlen" (pszeudovéletlen) szó azt jelenti, hogy ez a szám természetesen nem valódi véletlen érték, hiszen a gép valamilyen számítási módszerrel határozza meg, a véletlenszám-generátor minden egyes hívásakor, de a felhasználó szempontjából egy jó véletlenszám-generátor úgy tekinthető, mintha valódi véletlen értékeket szolgáltatna. Ez csak kivételes esetekben nem teljesül, például akkor, ha nagyon sok álvéletlen számot használunk a programunkban, és így előfordulhat, hogy a generátor ciklusba kerül, azaz ismét ugyanazokat a számokat kezdi előállítani. Akit bővebben is érdekelnek a számítógépes véletlenszám-generátorok, azoknak figyelmébe ajánlom a [11] és [12] irodaimat.

A véletlen, illetve álvéletlen számokat akkor nevezzük egyenletes eloszlásúaknak, ha e számok értéktartományában minden lehetséges előállított érték azonos valószínűségű. Nekünk a lottószámok sorsolásához éppen ilyen értékekre van szükségünk, hiszen a számok húzásánál 1-től 90-ig minden szám azonos valószínűséggel szerepelhet.
Ha az általunk használt gépen, illetve Pascal rendszerben nincs megfelelő véletlenszám-generátor, akkor használhatjuk például a következő függvényt. Ez 0 és 1 közötti valós álvéletlen számokat állít elő:

function random:real;
const m=259; { = sqrt(2*maxint+2)+3 }
begin
  ix:=ix*m;
  if ix<0 then ix:=ix+maxint+1;
  random:=ix/(maxint+1.0);
end;

E függvény használatához a következőket kell betartani:

ahol o, p és m egy-egy egész típusú változó.

A random függvényben használt m=259 a 16 bites gépeknél jó, vagyis ahol maxint=32767; 32 bitest; gépeknél pedig m=65539. (Fontos, hogy itt és a random függvényben is 1.0 - és ne 1 - szerepeljen a maxint mellett, mert így az összeadás valós típusú adatokkal történik.)

Ez a függvény maxint/4 különböző értéket tud generálni, tehát csak ennyi hívás után ismétli a számokat.
Itt ismét érdemes megemlíteni, hogy gyakorlatilag minden Pascal-megvalósításban van valamilyen, álvéletlen számok előállítására alkalmas eljárás vagy függvény. Ezek általában jobbak, mint a fent meg-adott egyszerű függvény, ezért célszerű őket használni. A további példaprogramokban nem használjuk a fenti függvényt, feltételezzük, hogy a használt megvalósításban létezik az előre definiált random függvény.
Miután van már módszerünk véletlenszámok előállítására, elkészíthetjük a programot, amely meglehetősen egyszerű (és a halmaz műveletek bemutatásakor még egyszerűbben el fogjuk készíteni). Csak arra kell figyelni, hogy a kisorsolt 5 szám különböző legyen.

program lotto;
var uj,k,b:integer;
    szamok:array[1..5] of integer;
    vanilyen:boolean;
begin
  for k:=1 to 5 do begin  { szamok ciklusa }
    repeat
      uj:=trunc(random*90+1); { kovetkezo veletlen ertek }
      vanilyen:=false;
      for b:=1 to k-1 do  { volt mar ilyen? }
        if szamok[b]=uj then vanilyen:=true
    until not vanilyen;   { egyik elozovel sem egyezik }
    szamok[k]:=uj;
    write(uj:4)
  end;
  writeln
end.

Az uj változó értékének kiszámításakor a random által szolgáltatott 0 <= x1 < 1 értéket megszoroztuk 90-nel, majd hozzáadtunk 1-et, így egy 1 <= x < 91 valós értéket kaptunk. Ebből a trunc függvénnyel csonkítva kaptuk meg a kívánt 1<=u j <=90 egész számot.

Ezt a feladatot a 6.2. fejezetben egyszerűbben is meg fogjuk oldani.

3.4.5. Feladat:
Alakítsa át lotto programot úgy, hogy a kisorsolt számokat nagyság szerint növekvő sorrendben írja ki!

Kíváncsiak vagyunk, hogy az előbbi módszerrel előállított lottószámok valóban egyenletes eloszlásúak-e azaz a 90 lehetséges szám valóban közel azonos gyakorisággal keletkezik-e. Ez elsősorban attól függ, hogy véletlenszám-generátor függvényünk valóban egyenletes eloszlású számokat állít-e elő a (0,1) nyílt intervallumban.

3.4.3. Példa
Készítsünk programot, amely megvizsgálja, hogy a random véletlenszám-generátorral előállított értékek mennyire egyenletes eloszlásúak!

Megoldás:
Felosztjuk a (0,1) tartományt k egyenlő részre, és a program segítségével megszámoljuk, hogy n véletlenszámból hány esik az egyes intervallumokba, k és n bemeneti adat. Az eredményt hisztogram formájában jelenítjük meg, karaktergrafikus ábrán, amelynél a függvényértéket az adott intervallumba eső véletlenszámok száma adja.

program rndteszt;
const kmax=70;
      magassag=20; { abra magassaga }
var n,k,b,g,x,max:integer;
    gy:array[0..kmax] of integer;  { gyakorisagok }
    a:real;  { aranyossagi tenyezo }
begin
  Randomize;  { Turbo Pascalban }

  { k es n beolvasasa }
  repeat
    write('Intervallumok szama (2...',kmax,') = ');
    readln(k);
  until (k>=2) and (k<=kmax);
  write('Kiserletek szama = ');readln(n);
  for g:=0 to kmax-1 do
    gy[g]:=0;

  { kiserletek elvegzese }
  for b:=1 to n do begin
    x:=Trunc(Random*k);
    gy[x]:=gy[x]+1 { szamlalas }
  end;

  { hisztogram kirajzolasa }
  writeln;writeln;
  max:=gy[1];  { gy legnagyobb elemenek megkeresese }
  for g:=2 to k-1 do
    if gy[g]>max then max:=gy[g];
  a:=magassag/max;  { aranyossagi tenyezo a rajzhoz }
  for g:=magassag downto 1 do begin { abra sorai }
    write(Round(g*max/magassag):5,' ');
    for b:=0 to k-1 do begin
      x:=Round(a*gy[b]); { eddig nyulik fel a b-edik oszlop }
      if g=x then write('*')  { oszlop vege }
      else if g<x then write(':')
        else write(' ')
    end;
    writeln
  end;
  { intervallumok sorszamai az abra ala }
  writeln;write(' ':7);
  for g:=1 to (k-1) div 10 do
    write(g:10);
  writeln;write(' ':6);
  for g:=0 to k-1 do
    write(g mod 10:1)
end.

Például k=70 és n=2000 esetén a következő ábrát kaphatjuk:

Bizonyítványok átlagainak kiszámítása

3.4.4 Példa:
Készítsünk programot, amely a 3.3. fejezetben bemutatott adatszerkezetbe beolvasott alapján meghatározza a gimnázium minden tanulójának, osztályának, évfolyamának, végül az egész gimnáziumnak az átlagát!

Megoldás:
Mivel az osztálylétszám, illetve a tantárgyak száma osztályonként változó lehet, ezeket az adatokat is tárolnunk kell. Ehhez tt típusú tömböket használunk. Az adatok beolvasását azzal könnyítjük meg, hogy mindig kiírjuk, melyik adatot, illetve adatokat várjuk.
A tömbök méretét csökkentettük, mert a többdimenziós tömbök "falják a memóriát", 8 bites gépeken, Turbo Pascalban így is csak futtatható állományba fordíthatjuk le a programot.

program atlagok4;
const evszam=4; { evfolyamok szama }
    utoszt='c'; { utolso parhuzamos osztaly jele }
    maxletszam=28;  { maximalis osztalyketszam }
    maxtantszam=12; { tantargyak maximalis szama }
type tt=array[1..evszam,'a'..utoszt] of integer;
    evibizi=array[1..maxtantszam] of integer;
    bizonyitvany=array[1..evszam] of evibizi;
    osztaly=array[1..maxletszam] of bizonyitvany;
    ev=array['a'..utoszt] of osztaly;
    gimi=array[1..evszam] of ev;
var tantargyszam,letszam:tt;
    revay:gimi; { a gimnazium osszes bizonyitvanya }
      { letszam, jegyek osszege.. }
    gimnjszam,gimnosszeg, { ...a gimnaziumban, }
    evjszam,evosszeg,     { ... egy evfolyamban, }
    osztjszam,osztosszeg, { ... egy osztalyban }
    tosszeg, { jegyek osszege egy tanulonal }
    xev,xtan,xtant:integer;
    xoszt:char;
begin

{ adatok beolvasasa }
  writeln('Bizonyitvanyok adatainak beolvasasa kovetkezik.');
  for xev:=1 to evszam do { evek ciklusa }
    for xoszt:='a' to utoszt do begin { osztalyok ciklusa }
      writeln;
      writeln(xev:2,'.',xoszt,' osztaly adatai kovetkeznek');
      write('Tantargyak szama = ');
      read(tantargyszam[xev,xoszt]);
      write('Osztalyletszam = ');read(letszam[xev,xoszt]);
      for xtan:=1 to letszam[xev,xoszt] do begin { tanulok }
        writeln(xtan:4,'. tanulo jegyei (',
                tantargyszam[xev,xoszt],' darab) : ');
      for xtant:=1 to tantargyszam[xev,xoszt] do
        read(revay[xev,xoszt,xtan,xev,xtant])
    end
  end;

  { atlagok kiszamitasa }
  writeln;writeln(' ATLAGOK: ');
  gimnjszam:=0;gimnosszeg:=0;
  for xev:=1 to evszam do begin { evek ciklusa }
    evjszam:=0;evosszeg:=0;
    for xoszt:='a' to utoszt do begin { osztalyok ciklusa }
      writeln;writeln(xev:2,'.',xoszt,' osztaly: ');
      osztosszeg:=0;
      for xtan:=1 to letszam[xev,xoszt] do begin
        tosszeg:=0;
        for xtant:=1 to tantargyszam[xev,xoszt] do
          tosszeg:=tosszeg+revay[xev,xoszt,xtan,xev,xtant];
        writeln(xtan:4,'. tanulo atlaga = ',
                tosszeg/tantargyszam[xev,xoszt]:4:1);
        osztosszeg:=osztosszeg+tosszeg
      end;
      osztjszam:=letszam[xev,xoszt]*tantargyszam[xev,xoszt];
      writeln;writeln(xev:2,'.',xoszt,
           '. osztaly osszes jegyenek atlaga = ',
      osztosszeg/osztjszam:4:1);
      evosszeg:=evosszeg+osztosszeg;
      evjszam:=evjszam+osztjszam
    end;
    writeln;writeln(xev:2,'. ev osszes jegyenek atlaga = ',
         evosszeg/evjszam:4:1);
    gimnosszeg:=gimnosszeg+evosszeg;
    gimnjszam:=gimnjszam+evjszam
  end;
  writeln;writeln('A gimnazium osszes jegyenek atlaga = ',
       gimnosszeg/gimnjszam:4:1)
end.

3.4.6. Feladat:
Az előbbi programban a nagyobb egységek (osztályok, évfolyamok, gimnázium) átlagát nem az eggyel alacsonyabb szintű egységek átlagainak átlagaként számítottuk ki, hanem az összegek összegét osztottuk a jegyek számának összegével.
Alakítsa át a programot úgy, hogy a nagyobb egységek átlagát az eggyel kisebb egységek átlagának átlagaként határozza meg!

3.4.7. Feladat:
Egészítse ki a programot a bemeneti adatok ellenőrzésével!

3.4.8. Feladat:
Az atlagok4 programban a beolvasás és az átlagszámítás külön történt, pedig ezek ugyanolyan ciklusok megszervezését igénylik. Alakítsa át a programot úgy, hogy a beolvasás és az átlagszámítás közös ciklusban történjen!

3.4.9. Feladat:
Egészítse ki az atlagok4 programot azzal, hogy meghatározza a teljes gimnáziumra az egyes párhuzamos osztályok együttes átlagát is: az A osztályok közös átlagát, a B osztályokét, a C osztályokét.
Készítse el ezt a kiegészítést a 3.4.8. feladat szerint átalakított programban is!

3.4.10. Feladat:
Alakítsa át az atlagok4 programot úgy, hogy olyan iskola adatait is fel tudja dolgozni, amelynél a párhuzamos osztályok száma nem minden évfolyamon azonos!

3.4.11 . Feladat:
Alakítsa át az atlagok4 programot úgy, hogy a bukásokat is figyelembe lehessen venni:

3.4.12. Feladat:
Egészítse ki a 3.4.11. feladat megoldását azzal, hogy elvégzi az új tanév kezdéséhez szükséges adminisztrációt: a bukottak kivételével a tanulók adatait eggyel magasabb évre viszi.

3.4.13. Feladat:
Egészítse ki az atlagok4 programot olyan programrésszel, amely csökkenő átlagok szerinti sorrendben kiírja az iskola

Deklaráljon megfelelő további adatokat, amelyek segítik a feladat gyors elvégzését!

3.5. Intervallum típus

A felvételi pontszámok feldolgozásánál, például a felveteli1 programban a pontszámokat olyan tömbben tároltuk, amelynek integer típusú elemei voltak, pedig a pontszámok értéke csak a 0..120 tartományban lehet.
A gimi típusú tömbben osztályzatokat tároltunk. Ez csak az 1..5 értékeket veheti fel, esetleg 0 értékkel jelezzük, ha valaki fel van mentve tornából. A jegyek értéktartománya csak 0..5, felesleges az integer-re megengedett sokkal nagyobb értékkészlet.
Ha tudjuk, hogy az értékeknek valamilyen tartományba kell esniük, akkor az adatok beolvasásakor ezt általában ellenőrizni is szoktuk, bár mintapéldáinkban az egyszerűség és áttekinthetőség kedvéért ezzel nem mindig foglalkoztunk.
A bizonyítványadatok feldolgozásakor, például az atlagok4 programban a tt, a bizonyitvany és a gimi tömbtípus-azonosító definiálásakor is használtuk az 1..evszam indextartomány-megadást, mindhárom esetben azonos értelemben, a gimnáziumi oktatás évfolyamainak a megadására. Ha a programot át akarnánk alakítani úgy, hogy az általános iskolai képzésnek megfelelően ez az index 1..8, vagy az egyetemek esetén 1..5, illetve 1..6 legyen, akkor nem elég segítség, ha az indextartomány felső határát konstanssal adjuk meg. Az is jó lenne, ha magát az indextartományt is használhatnánk névvel ellátva. Például így (csak a megváltozott részeket felsorolva):

type
  evindex=1..evszam; {evek indexenek típusa}
  tt=array[evindex,'A'..utoszt] of integer;
  bizonyitvany=array[evindex] of evibizi;
  gimi=array[evindex] of ev;
  ...

Az evindex típust intervallum típusnak nevezzük, mert az ilyen típusú mennyiség egy másik típusnak (itt az integer-nek) egy összefüggő intervallumába eső értékeit veheti csak fel.
Az intervallum típus definiálása nemcsak azért előnyös, mert egyértelműbbé, áttekinthetőbbé tehető vele a programunk, hanem azért is, mert a fordítóprogramnak lehetővé teszi, hogy kisebb helyet foglaljon a memóriában a kisebb értéktartományba eső adatok számára. A Turbo Pascal például azoknak az adatoknak, melyek nem lógnak ki a 0..255 tartományból, csak egyetlen byte-ot foglal le, míg az ennél nagyobb értékkészletű, sorszámozott típusú mennyiségeknek 2 byte-ot. Mivel a legtöbb személyi számítógép 1 byte-os adattal gyorsabban tud műveleteket végezni, mint 2 byte-osakkal, ez nemcsak jobb helykihasználással, de időmegtakarítással is járhat.


Az intervallum típus

1. Az intervallum típus egy sorszámozott adattípus egyetlen összefüggő résztartományát adja meg.

2. Alakja:

intervallum típus:

konstans..konstans

Például:

1..10
-5..5
'A'..'Z'
'0'..'9'
hetfo..pentek

(Feltételeztük a hetfo és pentek konstansok megfelelő definícióját.)

3. Szabályok:

4. Intervallum típusú adattal mindazok a műveletek elvégezhetők és mindazok a függvények használhatók, amelyek az alaptípusával, kivéve, hogy intervallum típusú változó, illetve függvény nem kaphat a megengedett értéktartományon kívüli értéket sem értékadással, sem read, readln vagy get eljárással.
Ha egy adott, intervallum típusú változó kifejezésben fordul elő, a kifejezés típusát úgy kell tekinteni, mintha benne az intervallum típusú változó helyett az alaptípusának megfelelő változó szerepelne. (b) (c)

Magyarázatok, megjegyzések:

(a) Mivel az intervallum típus alaptípusa csak sorszámozott típus lehet, nem lehet real intervallum típusát definiálni. Így tehát helyes az 1..5 intervallumtípus-megadás, de hibás az 1.0..5.0.

(b) Ez a szabály természetesen megengedi, hogy egy adott, intervallum típusú v változó olyan kifejezésben szerepeljen, amelynek értéke kilóg a v változóra megengedett intervallumból. Sőt, a kifejezés típusa v alaptípusától is különbözhet.
Például a

var v:1..10;
     r:real;
     g:integer;

deklarációk esetén megengedettek a következő értékadások:

g:=v+100; { a kifejezés értéke kilóg v értéktartományból, de g-ében benne van}
r:=v/2.5;   { v értékét valóssá alakítja, így végzi el az osztást}
v:=g;        { helyes, ha pl. g=10; hibás, ha pl. g=11)
v:=v+2;    { ha előtte v=8, akkor még helyes; ha v=9, akkor már hibás}

(c) Lényeges különbség az egészekre vonatkozó -maxint..maxint értékkorlát és a valódi intervallum típus között, hogy egész kifejezés értéke még átmenetileg sem léphet ki a maxint által meghatározott tartományból, az intervallum típus értékellenőrzése pedig csak az új érték tényleges tárolásakor történik meg.


Az intervallum típus használata nem helyettesíti a beolvasáskor szükséges ellenőrzést, hiszen ha egy szám kilóg a változó megengedett értéktartományából, az a program futásának megszakításához, vagyis az addig kiszámított értékek elvesztéséhez vezethet.
Az intervallum típust leggyakrabban tömbök indextípusának megadására használjuk. Van azonban egy másik nagyon fontos alkalmazása is, amelyet főleg programok tesztelésekor használunk. Ez pedig azon alapul, hogy amikor egy intervallum típusú változó értéket kap, a gép ellenőrzi, hogy az valóban a megengedett tartományban van-e. Ha tehát már a program megírásakor tudjuk, hogy milyen tartományban vehet fel értéket az adott változó, akkor megfelelő intervallum típus használata esetén nem kell külön utasításokkal ellenőriznünk a kapott érték helyességét, ezt a gép elvégzi helyettünk. Ennek az az ára, hogy a program mérete és futási ideje valamivel nagyobb lesz, illetve hiba esetén a program futása úgy szakad meg, hogy a hiba helyéről és jellegéről csak meglehetősen szűkszavú tájékoztatást kapunk.
Ha befejeztük a program kipróbálását, és nincs már szükség az intervallum típusú változók és tömb-indexek értékének ellenőrzésére, a fordítóprogramnak adott megfelelő utasítással (opcióval) megszüntethetjük az értékellenőrzést, így teljes hatékonyságú programot kapunk. Ez a fordítóprogramnak szóló utasítás nem tartozik bele a szabványos Pascal nyelvbe, hanem annak kiterjesztését, bővítését jelenti. A Turbo Pascal esetén például {$r- } formában tehetjük meg, ez a fordítóprogram az intervallum típus ellenőrzését nem is végzi el automatikusan, hanem csak akkor, ha ezt az előzővel ellentétes {$r+ } opcióval kérjük.

3.6. Szabványos típusok átdefiniálása, adatok kompatibilitása

Az integer típus olyan intervallum típusnak is tekinthető, amelynek definíciója:

integer=-maxint..maxint;

Hasonlóan, a logikai típust akár így is megadhatnánk:

boolean=false..true;

Ezeket az adattípusokat természetesen nem érdemes így definiálni, hiszen már eleve adottak, mégpedig pontosan ilyen értelemben. Felvetődik azonban a kérdés, hogy egyáltalán megtehető-e ez a definiálás. Ha igen, akkor mi lenne annak a hatása, ha egy programban például az egész típust így definiálnánk:

integer=-100..100;

Nos, a szabványos, más szóval előre definiált típusazonosítók ilyen átdefiniálása valóban megtehető, hatására a definíciót követően a szóbanforgó típusazonosító az újonnan definiált értelemben lesz használható. Példánkban egy ez után deklarált integer típusú változó már csak -100 és 100 közötti értékeket vehet fel. Ez nem jelenti azt, hogy az egész értékű kifejezések értéke átmenetileg sem lóghat ki ebből a tartományból, csupán azt, hogy amikor egy integer változó értéket kap, akkor ez az érték csak ebben a kisebb tartományban lehet.
A szabványos típusazonosítók ilyen átdefiniálásának nem sok értelme van, kivéve talán egy esetet. Képzeljük el, hogy olyan számítógépet használunk, amely az egész számokat 32 biten ábrázolja, így maxint=2147483647. De tudjuk, hogy a programunkat majd olyan gépeken is használni akarják, ahol az egészeket 24 biten tárolják, így maxint=8388607, és olyanokon is, ahol 16 biten, tehát maxint=32767. A programot tehát úgy szeretnénk megírni, hogy lehetőleg minden változtatás nélkül használható legyen ezeken a gépeken is, azaz már a program elkészítésekor kiderüljön, melyek azok a programrészek, amelyek a rövidebb egész számokat használó gépeken hibához vezethetnek, ezeknél aztán más megoldást kell találni.
Ezt a problémát a következő definíciók alkalmazásával tudjuk - részben - megoldani:

const maxint=32767;
...
type integer=-maxint..maxint;
...

Ilyen definíciók esetén minden integer típusú változóra vonatkozó értékadást ellenőrizni fog a gép: ha egy értékadásban kilóg egy adat ebből a szűkített értékkészletből, hibajelzés után megszakad a program végrehajtása, tehát előre látjuk, hogy hol lehet majd a kisebb gépeken baj. Ez a megoldás azonban nem nyújt teljes biztonságot, hiszen ha egy egész típusú kifejezés értéke kilóg az így átdefiniált tartományból, az nem okoz hibát, csak az, ha ezt az értéket bele is akarjuk tenni egy integer változóba. Így tehát a maxint és integer előbbi átdefiniálása után

var g,k:integer;

deklarációk esetén

g:=30000;
k:=3*g div 8;

nem okoz hibát a 32 bites gépen, hiszen csak a 3*g részkifejezés értéke lóg ki a megengedett tartományból, 3*g div 8 nem. A 16 bites gépen azonban már 3*g kiszámítása sem végezhető el. Az ilyen esetek felismerése tehát továbbra is a gondos programozó feladata marad.

A szabványos típusok átdefiniálása egészen különleges dolgokat is lehetővé tesz, ezeket azonban - ha csak nem akarunk magunknak nagyon kellemetlen, nehezen felderíthető hibákat - lehetőleg kerüljük el:

type
   t=integer;
   integer=real;
   real=t;
   ...
var v1:real;     { egesz tipusu lett }
     v2:integer; { valos tipusu lett }

Ebben a könyvben több helyen, főleg a nyelv szabályainak leírásában olyan kifejezések szerepelnek, mint "előre definiált integer típus" vagy "szabványos real típus". Ezekben esetleg feleslegesnek tűnhetett: az "előre definiált", illetve "szabványos" megkülönböztetés. Most már látható, hogy például az integer azonosító nem feltétlenül az előre definiált típust jelenti, a programban átdefiniált jelentéssel is használhatjuk. A szabályok leírásában azonban egyértelműen az előre definiált azonosítók eredeti, "előre definiált" jelentését használjuk, amely nem azonos az

integer=-maxint..maxint;

azonosítóval. Még akkor sem, ha maxint értékét nem definiáltuk át; igaz, az így definiált integer és az előre definiált integer típusok kompatibilisek.
Az eddigiek alapján már sejthetjük, hogy az adattípusok milyen központi szerepet játszanak a Pascal nyelv-ben, és azt is, hogy a típusok azonosságát, hasonlóságát, illetve a különböző típusú adatok kifejezésekben, illetve értékadó utasításokban való együttes használatát több dolog is meghatározza. Az ezzel kapcsolatos szabályokat két fogalom határozza meg: az adatok kompatibilitása, illetve értékadás-kompatibilitása. (A kompatibilitás szó magyar fordításban összeférhetőséget, együttes használhatóságot jelent, de speciális számítástechnikai jelentésére az angol szó terjedt el világszerte.)

Adatok kompatibilitásának kérdését az egyes adattípusoknál külön is részletezzük, itt az ezzel kapcsolatos két legfontosabb fogalmat adjuk meg.


Adatok kompatibilitása:

Kompatibilis típusok:
A t1 és t2 típust kompatibilisnek nevezzük, ha a következők bármelyike teljesül:

Értékadás-kompatibilis típusok:
A t2 típust értékadás-kompatibilisnek nevezzük a t1 típussal v1:=v2 irányban - ahol v1 t1 típusú változó, függvénynév, vagy formális paraméter, v2 pedig t2 típusú kifejezés - ha a következők bármelyike teljesül:

Bárhol, ahol az értékadás-kompatibilitási szabályt alkalmazzuk:

Magyarázatok, megjegyzések:

(a)

type
  at=array[1..10] of real;
  bt=array[1..10] of real;
  ar=record m1:real; m2:char; end;
  br=record m1:real; m2:char; end;

Talán meglepő, de az előbbi definíció szerinti at és bt típus egymással nem azonos, tehát nem is kompatibilis, és így nem értékadás-kompatibilis; hasonlóan ar és br sem. Ennek az az oka, hogy minden típusmegadással - tehát ha a típust nem közvetlenül típusazonosítóval, hanem például tömb esetén array ..., rekord esetén record ..., mutató típus esetén ^... alakban adjuk meg, - új, minden előzőtől különböző típust vezetünk be, még akkor is, ha az esetleg minden részletében megegyezik egy előző típussal.

(b) Bizonyos típusú adatok között az ord, chr, round, illetve trunc előre definiált függvények segítségével konverziót végezhetünk, ezek használatát az adott típusoknál tárgyaljuk.


3.7. A karakterlánc (string) típus

A gimnáziumi bizonyítványok átlagának kiszámításakor a tanulókat sorszámukkal különböztettük meg. Sokkal természetesebb és a valóságnak is jobban megfelel, ha a tanulók nevét is tároljuk egy megfelelő adatszerkezetben.
Egy név tárolásához általában 30 karakter elég, ezért a név(név) adattípust így definiálhatnánk:

type nev=array[1..30] of char;

Ez azonban több szempontból sem tökéletes megoldás.
Az első javítanivaló, hogy a név hosszát a program változtathatósága érdekében jobb egy előzőleg definiált konstansazonosítóval megadni.

Az ilyen adattípusnak az a második kellemetlen tulajdonsága, hogy két névnek semmilyen relációját nem tudjuk közvetlenül megvizsgálni; ahhoz pedig, hogy egy névsort ábécérendbe szedjünk, a <, > vagy <=, >= műveleteket is használnunk kellene.

A harmadik kellemetlen tulajdonság már nem ilyen magától értetődő, ennek felismeréséhez valamit tudnunk kell arról is, hogy a gép hogyan tárolja a karakteres adatokat. Bár egy karakter tárolására elég lenne egyetlen byte, azaz 8 bites adat is, sok gép egy-egy karaktert mégis külön szóban tárol, a szó pedig általában 16 vagy 32 bites, azaz 2 vagy 4 byte-os. Ennek a memóriapazarlásnak az az oka, hogy sok számítógép - belső működésének megfelelően - gyorsabban tud a memóriából egy szót elővenni, vagy oda beírni, mint annak egy részét, például egy byte-ot. (Igaz, modernebb gépeknél, főleg a személyi számítógépeknél ez egyre kevésbé van így.) Ha pedig egy karaktert a gép 4 byte-on tárol, akkor egy 30 karakteres tömb számára 30*4=120 byte szükséges. Mivel nagyon gyakori, hogy szövegeket is tárolunk, illetve dolgozunk fel számítógépek segítségével, ez a memóriapazarlás nem megengedhető. (A 8 bites mikroszámítógépek mind 1 byte-on tárolják a karaktereket, ezeken tehát ez a probléma nem merül fel.)

Az utóbbi két problémát, tehát a <, > stb. műveletek használatát és a memóriával való takarékoskodás megoldja a packed, azaz pakolt vagy más néven sűrített karaktertömbök használata:

const nevhossz=30;
...
type nev=packed array [1..nevhossz] of char;

A packed is kulcsszó, azt írja elő, hogy ha lehet, akkor az utána megadott összetett adattípust tömörítetten tetten kell tárolni. Már most meg kell azonban jegyeznünk, hogy az eleve 8 bites mikroszámítógépekre tervezett megvalósításokban (ide értve a HiSoft-Pascalt és a Turbo Pascalt-is), a packed kulcsszó ugyan - a kompatibilitás érdekében - implementált, de hatástalan. (A Turbo Pascalban helyette az $x direktívával írhatjuk elő a fordítóprogramnak, tömbök memóriafoglalás szerinti optimalizálását.)

Turbo Pascalban létezik előre definiált string típus, melynek használata lényegesen kényelmesebb és rugalmasabb, a szabványos Pascal ebben az alfejezetben bemutatásra kerülő megoldásánál. A string kezelést a Turbo Pascal több definiált eljárása és függvénye is segíti.

A karakterlánc adattípus olyan packed tömb, amelynek alsó indexhatára 1 és karakterekből áll. Erre a típusra további szabályok is vonatkoznak.


A karakterlánc, azaz string típus

1. Karakterláncokban többkarakteres adatokat, például szövegek egy-egy sorát, neveket, szavakat tárolunk. Ez lényegében a tömbtípus egy kiterjesztett, további műveleteket is lehetővé tevő változatának tekinthető.

2. Karakterlánc, azaz string típusúnak nevezzük a

packed array[1..n] of char

típusú adatot, ahol n>1; vagyis a pakolt, 1-től indexelt, legalább 2 elemű tömböt, amelynek elemtípusa az előre definiált char típus. Például:

packed array[1..80] of char

(A Turbo Pascalban és a HiSoft-Pascalban a packed kulcsszó hatástalan, így elhagyható.)

3. A karaktertánc első, bal oldali karakterének az ilyen tömb 1 indexű elemét, második karakterének a tömb 2 indexű elemét stb., a karakterlánc utolsó, jobb oldali karakterének a tömb n indexű elemét tekintjük, (a)

4. Karakterlánckonstans (stringkonstans) megadásának alakja:
A karakterlánckonstans értékét, azaz olyan konstanst, amely karakterláncot, más néven stringet ad meg, aposztrófok közé kell írnunk. Ha ebben magát az aposztróf karaktert akarjuk megadni, azt dupláznunk kell, ennek helyén a stringben egyetlen aposztróf kerül tárolásra.
Karakterlánc nem nyúlhat túl a sor végén tehát s kezdő és záró aposztrófnak is ugyanabban a sorban kell lennie.

A karakterlánckonstanson belül a gép a kis- és nagybetűket különbözőnek tekinti.

A stringkonstans hosszát az aposztrófok között megadott karakterek száma adja meg, egy aposztrófmegadást egy karakternek kell tekinteni.

Helyes karakterlánckonstansok, az általuk megadott karaktersorozatok és ezek hossza:

karakterlánckonstans megadott karaktersorozat hossz
'alma' alma 4
'kozte szokoz' kozte szokoz 12
'A Hard Day''s Night' A Hard Day's Night 18
'        ' (8 db szóköz) 8

Hibásak:

'ez hibas záró aposztróf hiányzik
'A' Torok Afiumrol' aposztróf megadáskor duplázni kell
'a' csak egy karakter

5. Karakterláncokkal a következő műveletek végezhetők:

= egyenlőség vizsgálata: két karakterlánc akkor egyenlő, ha a két lánc azonos indexű elemei rendre egyenlőek.
<> nem egyenlő viszony vizsgálata: az s1 és s2 karakterlánc akkor nem egyenlő, ha van legalább egy olyan k érték, amelyre igaz, hogy
   s1[k]<>s2[k].
<
kisebb: s1<s2 akkor teljesül, ha van olyan k index, hogy minden g<k indexre - ha van ilyen g - igaz, hogy
   s1[g]=s2[g] és s1[k]<s2 [k].
Ez azt jelenti, hogy a karakterláncokban balról jobbra haladva az első különböző karakterpár olyan, hogy az s1-beli kisebb, mint az s2-beli. Például:

'alfa'<'alma'  mert  'f'<'m'

>
nagyobb: s1>s2 akkor igaz, ha van olyan k érték, hogy minden g<k értékre - ha van ilyen g - igaz, hogy
    s1[g]=s2[g] és s1[k]>s2[k].
Ezt úgy is kifejezhetjük, hogy ha a karakterláncokban balról jobbra haladva összehasonlítjuk az azonos indexű karakterpárokat, az első különbségnél az s1-beli karakter nagyobb az s2-belinél.
<= kisebb vagy egyenlő: s1<=s2 akkor igaz, ha s1<s2 vagy s1=s2 igaz.
>= nagyobb vagy egyenlő: s1>=s2 akkor igaz, ha s1>s2 vagy s1=s2 igaz.

Magyarázatok, megjegyzések:

(a) A Pascal nyelvben tehát egy adott karakterlánc mindig azonos számú karaktert tárol, ezt a típusa definiálásakor az indexhatár megadásával rögzítjük. Ha olyan stringet akarunk használni, amelynek a hossza változik a program futása során, akkor erre ajánlható például a következő rekord típus:

type lanc=record
   hossz:integer;
   szoveg:packed array[1..maxhossz] of char;
end;

Itt maxhossz megfelelően definiált konstans, például:

const maxhossz=80;

A rekord hossz mezőjében azt tároljuk, hogy pillanatnyilag a szoveg mező első hány eleme tárolja a tényleges szöveget.
Egy ilyen típusú sz változó például kényelmesen kiírható a következő módon:

write(sz.szoveg:sz.hossz)

HiSoft-Pascalban a string kiírására nem használható ez az alak, mert így nem tudunk kevesebb karaktert kiírni, mint a string aktuális hossza, de nincs is rá szükség, mert csak a "hasznos" karaktereket írja ki, így használható ez a forma helyette:

write(sz.szoveg)

(b) Teljes karakterláncot ezek értelmében text típusú állományból nem lehet egyetlen read vagy readln utasítással beolvasni, bár a legtöbb megvalósítás tartalmaz ilyen kiterjesztést. Ha mégsem, akkor például az

str=packed array[1..strhossz] of char;

típusú karakterlánc beolvasására használhatjuk a következő eljárást:

procedure readstr(var s:str);
var g:integer;
    vege:boolean;
begin
  g:=1; vege:=false; { elso karakter indexe }
  repeat
    if eof then vege:=true; { file vege }
    else vege:=eoln or (g>strhossz);
    if not vege then begin
      read(s[g]); { kov. karakter beolvasasa }
      g:=g+1; { index novelese }
    end;
  until vege;
  for g:=g to strhossz do { maradek helyre szokoz }
    s[g]:=' ';
end; { proc. readstr }

(Feltételezzük az strhossz konstans megfelelő definícióját.)

Hasonló módon olvasható be egy - az (a) megjegyzésben leírt - lanc típusú változó értéke is, de abban a hossz értéket is be kell állítani:

procedure readlanc(var s:lanc);
var g:integer;
begin
  with s do begin
    g:=1; { elso karakter indexe }
    while not eoln and not eof and (g<=maxhossz) do begin
      read(szoveg[g]); { kov. karakter beolvasása }
      g:=g+1; { index novelése }
    end;
    hossz:=g-1; { hossz beallitasa }
    for g:=g to lanchossz do
      szoveg[g]:=' '; {maradek helyre szokozok}
  end; { with }
end; { proc. readstr }

Fontos, hogy a szöveg után fennmaradó részt itt is feltöltsük szóközökkel, mert szövegek összehasonlításakor csak így kapunk jó eredményt.

(c) A karakterláncok összehasonlításának ilyen definíciója miatt ha két karakterlánc csak betűket tartalmaz, akkor az angol ábécé szerint előrébb álló minősül kisebbnek.
Ha az összehasonlítandó stringek csupa számjegyet tartalmaznak, akkor a kisebbik számot tartalmazó string minősül kisebbnek.

Bár a szóköz karakter ord értékére nem tartalmaz megkötést a Pascal nyelv szabványa, az mind az ASCII, mind az EBCDIC kódban kisebb az összes egyéb, szövegekben használt karakternél. így ha két string első karakterei megegyeznek, utána pedig az egyikben szóközök, a másikban pedig további nem szóköz karakterek következnek, akkor egy összehasonlításban a "rövidebbik" szöveg értéke minősül kisebbnek.


A karakterlánc típust tehát szokás string típusnak vagy füzér, esetleg karakterfüzér típusnak is nevezni.

A HiSoft-Pascalban nem működik a (b) pontban bemutatott megoldás karakterláncok beolvasására, helyette a HiSoft-Pascal saját - lényegesen kényelmesebb - kiterjesztést kínál karakterláncok beolvasására:

readln;read(stringvaltozo);

Ha a beolvasott karakterfüzér rövidebb, mint a változó típusa, a fel nem használt karakterek CHR(0) kóddal kerülnek feltöltésre (szóköz helyett - így a szövegfüzér kiírása is egyszerűbb). Ha hosszabbat adunk meg, a szövegfüzér csonkul, a vége elvész.

A sorvége jel nem beolvasható, mivel nincs olyan char típusú érték, amely a sorvége jelnek felelne meg (a szabványos Pascal nyelvben). Ezért egy karakterláncban egy szövegnek legfeljebb egy sora tárolható. Ha ugyanis például egy

szoveg:packed array[1..100] of char;

változóba

for k:=1 to 100 do read(szoveg[k])

utasítással olvasunk be 100 karaktert, és beolvasás közben sorvége jelhez érünk, akkor a 2.13 alfejezetben a read eljárásnál karakter beolvasására megismert szabály értelmében a következő beolvasott karakterbe szóköz kerül. Utólag tehát nem tudjuk megállapítani, hogy valóban szóközt olvastunk-e be, vagy a szoveg változó azért tartalmaz az adott helyen szóközt, mert ott a beolvasott szövegben egy sor végére értünk.
Azért, hogy egy szöveg kiírásakor a szöveg sorokra tagolását is vissza tudjuk adni, azt a módszert szoktuk használni, hogy a szöveg minden egyes sorát külön stringben tároljuk, akármilyen rövid legyen is egy-egy sor tényleges hossza. A string további részét szóközökkel töltjük fel, a szöveget pedig ilyen stringekből álló tömbben tároljuk.

3.7.1 Példa:
Készítsünk programot, amely szöveg sokszorosítására alkalmas: beolvassa a példányszámot és egy legfeljebb 300 soros szöveget, majd e szöveget kiírja a megadott példányszámban. Minden egyes példány kiírását új lapon kezdje!

Megoldás:
A szöveg beolvasását a karakterlánc típus szabályainál a (b) megjegyzésben ismertetett módszerrel végezzük (azaz feltételezzük, hogy a használt megvalósítás nem tartalmaz kiterjesztést karakterlánc beolvasására), a kiírás pedig már nem tartalmaz újdonságot:

program xerox (input,output);
const maxsorhossz=78; { egy sor maximalis hossza }
      maxsorszam=300; { sorok maximalis szama }
      laphossz=60;    { sorok max. szama egy lapon }
type sorindex=1..maxsorszam;
     karakterindex=1..maxsorhossz;
     sor=packed array[karakterindex] of char;  { sor tipusa }
var szoveg:array[sorindex] of sor;
    peldanyszam,peldany,sorokszama,k:integer;
begin
  write('Peldanyszam: ');readln(peldanyszam);

  { szoveg beolvasasa }
  sorokszama:=1;k:=1;
  while not eof and (sorokszama<=maxsorszam) do begin
    if eoln or (k>=maxsorhossz) then begin  { sor vege }
      for k:=k to maxsorhossz do
        szoveg[sorokszama,k]:=' ';  { maradek resze szokoz }
      sorokszama:=sorokszama+1;  { sorok szamlalasa }
      k:=1;  { uj sor eleje }
      readln
    end
    else begin
      read(szoveg[sorokszama,k]);  { kov. karakter beolvasasa }
      k:=k+1
    end
  end;
  sorokszama:=sorokszama-1;  { sorok tenyleges szama }
  if not eof then begin
    writeln('Tul sok sorbol all a szoveg!');
    halt; { programfutas megszakitasa }
  end;

  { szoveg kiirasa }
  for peldany:=1 to peldanyszaam do begin { peldanyok ciklusa }
    for k:=1 to sorokszama do begin  { sorok ciklusa }
      writeln(szoveg[k]);  { egy sor kiirasa }
      if k mod laphossz=0 then page  { 60 soronkent uj lap }
    end;
    if sorokszama mod laphossz<>0 then page  { uj lap }
  end
end.

A sorokszama változót azért nem deklarálhattuk sorindex, illetve a k változót karakterindex típusúra, mert előfordulhat, hogy ezek 1-gyel nagyobb értéket is felvesznek, ha a beolvasott szöveg túl sok sorból áll, vagy egy sor túl hosszú.
Az előbbi programban azt is figyeljük, hogy 60 soronként új lap elején folytatódjon a kiírás, mert a harmonikaszerűen összehajtogatott számítógéppapír (az ún. leporelló) hajtására írt szöveget nehéz lenne elolvasni. Ha pedig a sorok száma éppen 60 egész számú többszöröse, akkor két példány között nem kell lapot kezdeni, mert az előző kiírás végén már volt lapemelés.

3.7.1. Feladat:
Mi történik a fenti programban, ha egy sor hosszabb, mint maxsorhossz? Javítsa ki a programot úgy, hogy ennél hosszabb sorokat is szövegveszteség nélkül tudjon kezelni!

3.7.2 Feladat:
Mi történik a xerox programban, ha a szöveg hosszabb, mint maxsorszam? Egészítse ki a programot úgy, hogy ilyenkor a program csak hibaüzenetet írjon ki, a csonka szöveget ne sokszorosítsa!

3.7.2. Példa:
Készítsünk programot, amely beolvassa egy osztály névsorát, majd ábécérendbe szedi és kiírja!

Megoldás
A névsor beolvasása és kiírása már nem okozhat gondot, sőt a rendezésre is láttunk módszert, például a felveteli3 programban.
Most azonban más rendezési módszert választunk, ennek a neve: buborékrendezés.

A buborékrendezés lényege az, hogy a táblázat elejéről indulva rendre összehasonlítjuk a szomszédos elemeket, és ha a kisebb van hátrább, megcseréljük őket. Nézzük meg a buborékrendezés egy-egy menetét mintapéldán! Nyilak jelzik, hogy hol kerül sor két szomszédos elem cseréjére, minden menetben felülről lefelé haladunk:

A módszer onnan kapta a nevét, hogy a táblázat elemei úgy tekinthetők, mint különböző súlyú buborékok, mindegyik csak azt érzékeli, hogy milyen nehéz a felette, illetve alatta levő, és ha a felső nehezebb, akkor a két elem helyet cserél.
Miután a tömb elejéről indulva egyszer végigmentünk a tömbön ezzel a módszerrel, a legnagyobb elem biztosan a legutolsó, azaz a végleges helyére kerül, hiszen minden alatta lévőnél nehezebb, tehát helyet cserél vele. Az azonban egyáltalán nem biztos, hogy az első menet után a többi elem is jó sorrendben lesz, hiszen ha egy könnyű elem alul van, minden egyes menetben legfeljebb egy hellyel tud feljebb jutni. A legrosszabb eset ebből a szempontból az, ha a legkisebb elem induláskor a táblázat legalján van, mert ekkor n elemű táblázat esetén n-1 menetre van szükség. Az is látható, hogy most szerencsénk volt, hiszen mindössze 3 menet elégnek bizonyult. Ezt onnan ismerhetjük fel, hogy a következő menetben (amely már nincs az ábrán) egyetlen cserére sincs szükség.

A programban beolvasásra a HiSoft-Pascal módszerét alkalmazzuk, ha az általunk használt megvalósításban nincs kiterjesztés szövegfüzér beolvasására, az előző programalapján elkészítetjük a beolvasást végző programrészt.

program nevsor;
const nevhossz=30;  { egy nev karaktereinek max. szama }
      nevsorhossz=30;  { nevsor hossza }
type nev=array[1..nevhossz] of char;  { nev tipusa }
     nevindex=1..nevhossz;  { nevek indextartomanya }
var nevsor:array[nevindex] of nev;
    mnev:nev;  { segedelem a cserehez }
    utolso,also:nevindex;
    voltcsere:boolean;
    i:integer;
begin
  { nevsor beolvasasa }
  for i:=1 to nevsorhossz do begin
    write(i:2,'. nev: ');readln;read(nevsor[i])
  end;
writeln;

  { rendezes buborekmodszerrel }
  utolso:=nevsorhossz;  { utolso vizsgalando elem indexe }
  repeat { menetek ciklusa }
    voltcsere:=false;
    for also:=1 to utolso-1 do { egy menet }
      if nevsor[also]>nevsor[also+1] then begin  { csere }
        mnev:=nevsor[also];nevsor[also]:=nevsor[also+1];
        nevsor[also+1]:=mnev;
        voltcsere:=true
      end;
    utolso:=utolso-1;  { utolso elem a helyere kerult }
  until not voltcsere or (utolso=1);

  { nevsor kiirasa }
  for i:=1 to nevsorhossz do
    writeln(nevsor[i])
end.

A programban közvetlenül kihasználtuk, hogy a karakterláncok a > művelettel összehasonlíthatók.

Számítsuk ki a szükséges műveletek számát! A legrosszabb esetben n méretű táblázat esetén n-1 menetre van szükség, ezekben először n-1, majd n-2 stb., végül 2, majd 1 összehasonlítás történik. Így az összehasonlítások teljes száma n(n-1)-2 azaz körülbelül n^2/2, tehát négyzetesen függ a táblázat hosszától.
Kimutatható, hogy a cserék száma is n^2-tel arányos. [3]

További rendezési módszerekkel a rekord típus, majd az eljárások és függvények megismerése után foglalkozunk.

3.7.3. Feladat:
Egészítse ki a bizonyítványok adatait feldolgozó programot úgy, hogy tárolja a tanulók nevét is. Az átlagok kiszámítása után azok osztályonkénti kiírását az ábécérendbe szedett névsor szerint végezze.

3.7.3. Példa:
Készítsünk programot, amely diákszokás szerint kiírja az

      Ó
     IÓ
    CIÓ
   ÁCIÓ
  KÁCIÓ
 AKÁCIÓ
VAKÁCIÓ

szavakat! (Persze ékezetek nélkül.)

Megoldás:
Ennek a programnak kivételesen nincs bemeneti adata. A VAKÁCIÓ szót egy stringváltozóba tesszük, majd megfelelő részeit ciklusban írjuk ki:

program vakacio;
var szo:array[1..7] of char;
    ettol,ezt:1..7;
begin
  szo:='VAKACIO';
  for ettol:=7 downto 1 do begin
    write(' ':ettol);
    for ezt:=ettol to 7 do
      write(szo[ezt]);
    writeln
  end
end.

A string típus egyik érdekessége, hogy ez az egyetlen összetett adattípus a szabványos Pascal nyelvben, amelynek az értéke egyetlen konstanssal megadható. Vagyis ha valamilyen konstans értéket akarunk beletölteni, akkor azt egyetlen utasítással megtehetjük, nem kell elemeit egyenként feltölteni. A szigorú típusellenőrzés azonban itt is érvényes, hiszen a konstansnak pontosan annyi karaktert kell tartalmaznia, ahány karakteres a változó.
A karakterlánccal már az első programokban találkoztunk, hiszen a write utasításokban a kiírandó szöveget valójában karakterlánc-konstanssal adtuk meg, aposztrófok közé valamilyen karaktersorozatot írtunk,
Van azonban egy lényeges különbség a karakterlánc-változó és a karakterlánc-konstans között (azon kívül is, ami általában egy változó és egy ugyanolyan típusú konstans között van, tehát hogy a konstans értéke nem változtatható meg). Mégpedig az, hogy csak a teljes karakterlánc-konstansra lehet hivatkozni, elemeire, például index segítségével nem. így tehát hibás lenne a vakacio programban akár a

... write('VAKACIO'[ezt]) { hibas! }

akár a

const v='VAKACIO';
...
write (v[ezt]) { hibas! }

megoldás is. Jegyezzük meg, hogy csak változó, mégpedig csak tömb típusú változó indexelhető!
Ez a szabály természetesen megengedi, hogy a string típusú változót megfelelő számú karaktert tartalmazó konstansból töltsük fel:

const v='VAKACIO';
var szo:array[1..7] of char;
...
szo:=v;

Több Pascal-megvalósítás, így például a Turbo Pascal is lehetővé teszi olyan string típus használatát amely változó számú karaktert tartalmaz, továbbá azt is, hogy stringekkel több további, a szövegek feldolgozását kényelmessé tevő műveletet végezzünk. Ezek azonban erősen megvalósításfüggő tulajdonságok, ezért itt nem foglalkozunk velük, használatukat pedig programunk egyik gépről a másikra való átvihetősége érdekében egyébként is érdemes megfontolni.
A vakacio program megoldása Turbo Pascalban:

program vakacio;
const szo='VAKACIO';
var i,j:byte;
begin
  j:=length(szo);
  for i:=0 to j-1 do
    writeln(' ':j-i,copy(szo,j-i,j))
end.

3.8. Rekord típus

Aki átgondolta a 3.7.3. feladat megoldását - egy-egy osztály tanulóinak átlagát kellett ábécérendbe szedet névsor szerint kiírni -, az találkozott azzal a problémával, hogy a névsorba rendezéskor két név felcserélésével együtt a megfelelő két átlagot is fel kell cserélni.
Az egyszerűség kedvéért most csak egyetlen osztály adataival foglalkozunk:

...
const nevhossz=30 { nev maximalis shossza }
   maxletszam=35; { maximalis osztalyletszam }
    ...
type nev=array[1..nevhossz] of char;
   tanuloindex=1..maxletszam;
   nevsortip=array[tanuloindex] of nev; { nevsor tipusa }
   atlagtip=array[tanuloindex] of real; { atlagok tipusa }
    ...
var nevsor:nevsortip; { egy osztaly nevsora }
   atlag:atlagtip; { osztaly atlagai }
   snev:nev;
   satl:real; { segedvaltozok a cserehez }
   k,g:integer;
...
   { a g-edik es k-adik elem csereje }
   snev:=nevsor[g];satl:=atlag[g];
   nevsor[g]:=nevsor[k];atlag[g]:=atlag[k];
   nevsor[k]:=snev;atlag[k]:=satl;

Mivel név és jegyátlag különböző típusú, eddig nem tudtuk egy-egy adott tanuló nevét és átlagát egyetlen közös adatelemben tárolni. Pedig ha volna ilyen adatelem, akkor két hallgató adatainak felcserélése egyetlen utasításhármassal elvégezhető lenne. A rekord szó a programozásban nem valamilyen csúcsteljesítményt jelöl, hanem egy olyan összetett adattípust, amelynek az elemei egymástól eltérő típusúak is lehetnek.
Az előbbi feladathoz például ilyen rekord típust, illetve változókat lehetne használni:

type tanulo=record { egy tanulo adatai }
          neve:nev;
          atlaga:real
       end;
    ...
var osztaly:array[tanuloindex] of tanulo; { osztaly adatai }
   stan:tanulo; { segedelem cserehez }
...
   { a g-edik es k-adik elem csereje }
   stan:=osztaly[g];
   osztaly[g]:=osztaly[k];
   osztaly[k]:=stan;

A tanulo típusú adat tehát kételemű, a hallgató nevét (amely maga is összetett típusú, hiszen karakterlánc) és a tanuló jegyeinek átlagát tartalmazza.
A rekordok elemeit mezőknek nevezzük. A mezőknek azonosítójuk van, a tanulo típusú rekordban például neve és atlaga a két mező azonosítója.
A rekord egy mezőjére úgy hivatkozhatunk, hogy a rekord megadása után pontot teszünk, majd a mező azonosítóját írjuk.
Például az stan változó mezőire

stan.neve

illetve

stan.atlaga

formában hivatkozhatunk, az előbbi nev, az utóbbi real típusú adatot jelent.
Mivel a nev karakterlánc típusú, ennek egy-egy elemére index segítségével lehet hivatkozni, így az stan változóban tárolt név 6. karakterét

stan.neve[6]

alakban tudjuk megadni.
Teljesen hasonló módon, az osztály 13. tanulójának átlagát és nevét

osztály[13]. atlaga

illetve

osztaly[13].neve

adja meg, a k-adik tanuló nevének 2*g-3-adik karakterét pedig

osztaly[k].neve[2*g-3]

formában tudjuk elérni.

A rekordhivatkozást és a mezőnevet elválasztó pont előtt, illetve után hagyhatunk szóközt vagy szóközöket is, ha úgy látjuk, hogy ez javítja a program szövegének olvashatóságát, de nem kötelező.

Most nézzük meg, hogy az eddigi példákban hol lett volna érdemes rekordot használni!
A felveteli2 és felveteli3 programban a felvételizők pontszám szerinti sorrendjének megállapításakor párhuzamosan változtattuk a pontszamok és sorszamok tömböt, helyette használhatnánk egyetlen

telem=record
        pontszam:0..120;
        sorszam:1..maxletszam;
      end;

típusú elemekből álló

t:array[1..maxletszam] of telem;

tömböt.
A hosszú egész számok tárolására használt tömb lényegében kétféle adatot tartalmaz: a nulla indexű elem az értékes számjegyek számát adja meg, az 1...hossz indexű elemek pedig a számjegyeket. Az ennek megfelelő adattípus:

szamjegyindex=1..hossz;
hosszu3=record
          ertekes:szamjegyindex;
           jegyek:array[szamjegyindex] of 0..99;
        end;

Itt hossz megfelelően definiált konstans, és kihasználtuk azt is, hogy e szám 100-as számrendszerben van felírva, így egy-egy számjegy értéke csak 0 és 99 között lehet. Ha az előjelet is tárolni szeretnénk, arra több mód is adódik:

hosszu4=record
          elojel:char; { '+' vagy '-' }
          ertekes:szamjegyindex;
           jegyek:array[szamjegyindex] of 0..99;
        end;

hosszu5=record
          negativ:boolean; { igaz, ha a szam negativ }
          ertekes:szamjegyindex;
           jegyek:array[szamjegyindex] of 0..99;
        end;

A 3.4.4. példában (atlagok4) egy adott évre vonatkozó bizonyítványba a jegyek átlagának és a bizonyítvány minősítésének is be kell kerülnie:

tantargyindex=1..maxtantszam; { tantárgyak indextípusa }
evibizi=record
        jegyekszama:tantargyindex;
        jegyek:array[tantargyindex] of 1..5;
        atlag:real;
        minosites :1..6; { 6:kitűnő 5:jeles 4:jó ...}
      end;

A bizonyítványban pedig benne kell lennie a tanuló legfontosabb személyi adatainak is, ez különböző típusú adatok tárolását jelenti, ezért érdemes külön rekordot definiálni a számára:

nev=array[1..nevhossz] of char;
datum=record
        ev:1900..2100;
        ho:1..12;
        nap:1..31;
      end;
   szemelyi_adatok=record
        neve,apja_neve,anyja_neve,szuletesi_hely:nev;
        szuletesi_datum:datum;
        lakcime:record
           iranyitoszam:0..9999;
           helyseg,utca:nev;
             hazszam,emelet,ajto:integer;
        end;
      end; {szemelyi adatok}
bizonyitvany=record
         sz_adatok:szemelyi_adatok;
         eredmenyek:array[1..evszam] of evibizi;
      end;

A szemelyi_adatok rekordban a különböző nevek és a lakcímhez szükséges számok megadásakor több mezőnevet is felsoroltunk vesszővel elválasztva. Ezek azonos típusú mezőket jelentenek, de ez az írásmód rövidebb, mint ha külön-külön definiáltuk volna a mezőket:

... record
   neve:nev;
   apja_neve:nev;
   anyja_neve:nev;
   szuletesi_hely:nev;

A karakterlánc típus tárgyalásakor, például a xerox programnál láttuk, hogy többsoros szöveg feldolgozásakor kénytelenek vagyunk minden egyes sort külön karakterláncban tárolni, ha pedig egy sor rövidebb, mint a karakterlánc, akkor a maradék részt szóközökkel kellett feltölteni. A maradék rész feltöltését és kiírását is megtakaríthatjuk, ha a karakterlánc típus szabályainál az (a) megjegyzésben ajánlott rekordot használjuk a szöveg egy-egy sorának tárolására:

const maxhossz=...; { karakterlanc max. hossza }
type sor=record
       hossz:0..maxhossz; { tenyleges hossz }
       szoveg:array[1..maxhossz] of char;
end;

Itt a szoveg mezőnek csak az első hossz karakterét kell feltölteni, kiíráskor pedig csak ezt kell kiír-ni. Ezt nagyon egyszerűen megtehetjük, például az

s:sor;

deklarációjú s változót használva:

write(s.szoveg:s.hossz)

Ez a karakterlánc első s.hossz karakterét írja ki, hiszen a kiírásra vonatkozó mezőhosszúságot így adtuk meg.

Mielőtt további példákat néznénk a rekordok használatára, ismerkedjünk meg a rekord típus egy érdekes további lehetőségével!

"...szokatlanul hosszú nyakán olyan fej ült,
melyből négy arc nézett az egyes égtájak felé...
gazdájuk mindenkor azt az arcát tudta előre fordítani,
amelyik a pillanatnyi lelki állapotának megfelelt. "

[0] 306. oldal

Változó rekord
Nagyon gyakori, hogy számítógépek segítségével emberek személyi adatait dolgozzuk fel. Szükség van erre vállalati vagy lakossági információs rendszerekben. De ha van otthon saját személyi számítógépünk, akkor mi is kényelmesen és hatékonyan tudjuk számon tartani barátaink telefonszámát, névnapját vagy egyéb fontos adatait.
Személyi adatok megadásával már a bizonyítvány esetében is találkoztunk, de ott csak a legfontosabb dolgokat vettük figyelembe. Ha például egy személyi nyilvántartó rendszer számára kell megfelelő leírási módot kidolgozni, további jellemzők tárolására is szükségünk lesz. Ilyen például a családi állapot, illetve ettől függően további adatok:

type szemelyi_adatok=record
          neve,apja_neve,anyja_neve,szuletesi_hely:nev;
          szuletesi_datum:datum;
          lakcime,munkahely_cime:record
              iranyitoszam:0..9999;
              helyseg,utca:nev;
              hazszam,emelet,ajto:integer
          end; { lakcíme }
          case csaladi_allapot:char of
              'e': (); { egyedulallo }
              'h','v': { hazassagban el, elvalt }
                 (hazastars_neve:nev;
                  gyszam:0..9;
                  gyerekek:array[1..9] of nev)
       end; { szemelyi adatok }

Ez a rekord tartalmaz egy úgynevezett változó részt is, amely a case kulcsszóval kezdődik. Az elején a csaladi_allapot mező - amely karakter típusú - adja meg az adott személy családi állapotát. Ha ez 'e' értékű, akkor az illető egyedülálló, ha 'h' értékű, akkor házas, ha pedig 'v' értékű, akkor elvált. Ezt a mezőt vezérlőmezőnek is szokás nevezni, mert ennek az értéke határozza meg a rekord további részének szerkezetét.
Az of kulcsszót követően a vezérlőmező típusának megfelelő konstansok az egyes változatoknak megfelelő vezérlőmező-értékeket adják meg, majd a három lehetséges változatot megadó további mezők következhetnek. Ha például a csaladi_allapot vezérlőmező értéke 'v' vagy 'h', akkor tároljuk még a ('v' esetén előző) házastárs nevét, a gyerekek számát és nevük tárolására egy 9 elemű tömböt. Egyedülálló személynél pedig nem tárolunk semmilyen további adatot. Látjuk azt is, hogy a rekord különböző változatainak megfelelő mezők megadását zárójelbe kell tenni, és a zárójeleket akkor is kikeli írni, ha az adott változathoz nem tartozik egyetlen mező sem. Ha két változathoz ugyanolyan mezők tartoznak, akkor ezeket együtt is megadhatjuk, sőt ez az igazán kényelmes megoldás, hiszen nem is lehetne két változatban ugyanolyan nevű mező.
Az egyes változatokhoz tartozó mezőkre ugyanúgy hivatkozhatunk, mint az előtte álló, úgynevezett állandó rész mezőire:

var s:szemelyi_adatok;

esetén például:

s.csaladi_allapot karakter típusú mező, és ha értéke 'h', akkor
s.hazastars_neve a házastárs nevét,
s.gyszam a gyerekek számát,
s.gyerekek[3] a harmadik gyerek keresztnevét adja meg.

Ezekre a mezőkre nem hivatkozhatnánk, ha s.csaladi_allapot='e' lenne, hiszen ehhez a változathoz nem tartoznak ilyen mezők.

HiSoft-Pascalban nem használhatunk változó rekordokat.

Mielőtt mintapéldákon is megnéznénk a rekordok használatát, ismerjük meg a rájuk vonatkozó szabályokat!


A rekord (record) típus

1. A rekord típus különböző típusú adatok együttes kezelését teszi lehetővé: a rekord elemei, azaz mezői ugyanis különböző típusúak is lehetnek. (a)

A mezők tehetséges száma a rekord definíciója által rögzített, azaz a program végrehajtása során nem változik. (b)

Nem szabványos típus, azaz minden rekordot deklarálnunk kell a programban.

2. A rekord típus megadásának formai szabályai:

rekord típus:

mezőlista:

állandó rész:

mezőcsoport:

mezőnévlista:

ahol mezőnév: azonosító

változó rész:

változatkijelölő:

ahol vezérlőmező: mezőnév,

vezérlőtípus: sorszámozott típus azonosítója

változat:

case-konstans lista:

ahol case-konstans: konstans

Példák:

type datum=record
          ev:integer;
          ho:1..12;
          nap:1..31
       end;

szemely=record
          nev,anyjaneve:str;
          szuletett:record
             hol:str
             mikor:datum
          end;
          case neme:nem of
             ferfi: (kora:integer);
             no: (szemeszine:szin;
                   fomeretek:array[1..3] of real
       end;

(Feltételezzük az str, nem, szin típusazonosítók megfelelő definícióját.)

3. A mezőlistáról:

Ha van változó rész, akkor fennállnak még a következők is:

4. Rekorddal, tehát teljes rekord típusú adattal a következő műveletek végezhetők:

5. Mezőhivatkozás a rekord típusú változó valamelyik mezőjét jelöli ki. Alakja:

mezőhivatkozás:

Önmagában a mezőnévvel csak with utasításban hivatkozhatunk a kijelölt rekord egy mezőjére. (Ezzel az esettel a with utasítás ismertetésénél foglalkozunk részletesen.)

6. Rekord mezőjével mindazok a műveletek végezhetők, amelyeket annak típusa megenged, két kivétellel:

Magyarázatok, megjegyzések:

(a) Definiálható természetesen olyan rekord is, amelynek mezői azonos típusúak, például:

r=record
     a,b,c:real;
     d:real
end;

(b) Ez a szabály csak az összes lehetséges értékek számát rögzíti, de hogy a program végrehajtásának adott pillanatában egy rekord hány mezője tárol valóban értéket és hány meghatározatlan, az csak a végrehajtás során dől el.

(c) A változó rész tehát mindig az állandó rész után áll, de bármelyik hiányozhat is. Ha akár az állandó, akár a változó rész szerepelt, a rekord végén megadható egy (egyébként felesleges) pontosvessző.

(d) A mezőcsoport tehát egy vagy több azonos típusú mező megadását jelenti, például:

a:real;
b:real;
c,d:real;

helyett használható:

a,b,c,d:real;

(e) Fontos, hogy a vezérlőtípus csak típusazonosítóval, méghozzá sorszámozott típus azonosítójával adható meg, azaz nem használható például típusmegadás a következő formában:

1..5
(elso, masodik, harmadik)

ezek ugyanis új intervallum-, illetve felsorolt típust adnának meg.
A rekord többi mezőjének típusa akár típusazonosítóval, akár a típus részletes definiálásával megadható, mint például a 2. pontbeli példában a szuletett és a fomeretek mezők.

(f) A case-konstans tetszőleges, a mezőtípussal kompatibilis sorszámozott típusú konstans, tehát egész, karakter, logikai vagy felsorolt típusú lehet, de nem lehet például stringkonstans, hiszen az összetett típus.

(g) Az így definiált datum típusú rekord tehát csak állandó részt tartalmaz és 3 mezője van. Ezek mindegyike a saját típusán belül, a többi mező értékétől függetlenül tetszőleges értéket vehet fel. Tehát a nyelv nem tiltja, hogy a ho mező értéke 2, ugyanakkor a nap mezőé akár 31 legyen.

(h) A szemely rekord állandó része 3 mezőből áll, a szuletett mező maga is rekord, ennek mikor mezője szintén rekord típusú.
A változó rész vezérlőmezője, a neme két változat lehet: ha az illető személy férfi, akkor korát tároljuk, ha nő, szeme színét és legfontosabb testméreteit. Így a neme mező értékétől függően a teljes rekord szerkezete a következőképpen írható le:

neme=ferfi eset:
neme=no eset:
nev,anyjaneve:str;
szuletett:record
             ...
             end;
neme:nem;

kora:integer;
nev,anyjaneve:str;
szuletett:record
             ...
             end;
neme:nem;

szemeszine:szin;
fomeretek:array[1..3] of real;

vagyis az állandó rész és a változó rész, sőt a vezérlőmező mezőnevei is "azonos szinten" vannak.

(i) Mint minden azonosítónak, hatáskörén belül a mezőnévnek is csak egy jelentése és így egy definíciója lehet. Tehát ugyanaz a mezőnév nem fordulhat elő kétszer ugyanazon rekord definíciójában, még akkor sem, ha az egyik az állandó részben, a másik a változó részben van.
Így tehát hibás:

record
   a,b :integer;
   c,a :boolean;  { a ismételten előfordul }

case integer of
   1: (p,q:real) ;
   22: (z,c:char)  { c ismételten előfordul }
end;

Megengedett azonban a következő:

record
   a,b:integer;
   c:record
      a,c:real;
      f:char
   end;
   f:boolean
end;


{ a es... }
{ c a kulso rekord mezoje }
{ a,c es... }
{ f a belso rekord mezoje }
{b elso rekord vége}
{ f a kulso rekord mezoje }

Ugyanis a és c első és f második előfordulása a külső rekord egy-egy mezőjét adja meg, míg a és c második, valamint f első előfordulása a belső rekord mezőjét jelenti, tehát teljesül, hogy ezek mindegyike csak egyszer fordul elő az őt közvetlenül tartalmazó rekordban.

(j) A vezérlőmező tehát önmaga is mezője az őt közvetlenül tartalmazó rekordnak, mégpedig mindig eleme annak, függetlenül attól, hogy a változó rész melyik változata aktív. Bár így maga a vezérlőmező az állandó rész mezőihez hasonlóan mindig elérhető, mégis a változó rész elemének tekintjük.

(k) Az (i) megjegyzésben leírtakhoz hasonló a helyzet a case-konstansok hatáskörére vonatkozóan is:
Hibás például:

record ...; { rekord eleje }
case integer of { valtozo resz eleje }
   1: (...);
   5,8,11,6: (...);
   3,5,1: (...)
   { Hiba: 1 es 5 ismetelten elofordul }
end; { rekord vége }

de helyes a következő:

record  { kulso rekord eleje }
   ...
   case c:char of  { valtozo resz eleje }
      'a','e','i': (...);
      'b','q': (...);
      'z': r2: (record case char of
         { belso rekord eleje }
         'q','a':(...);
         { belso rekordban vannak }
         ...
      end;)  { belso rekord vege }
end;  { kulso rekord vege }

(l) Ha feltételezzük a

v:szemely

változódeklarációt a 2. pontbeli típusdefiníciókkal, akkor ennek egyes mezői pl. így hivatkozhatók:

ha neme=ferfi, akkor: v.nev
v.anyjaneve
v.szuletett
v.neme
v.kora
ha neme=no, akkor: v.nev
v.anyjaneve
v.szuletett
v.neme
v.szemeszine
v.fomeretek

Ha valamelyik mező maga is összetett típus, akkor annak elemei a típusának megfelelő módon hivatkozhatóak, pl.:

v.nev[6]
v.szuletett.hol
v.szuletett.hol[1]
v.szuletett.mikor.ev
v.fomeretek[1]
a név 6. karaktere
str típus, a születési hely
a születési hely 1. karaktere
születési év
magasság, csak ha v.neme=no

3.8.1 Példa:
Készítsünk programrészletet, amely olyan karácsonyi üdvözlő sorokat ír ki, amelyek szövege igazodik a címzett családi állapotához! A címzésben a teljes nevet és címet, a levél szövegében pedig csak a keresztneveket használjuk, ezekről feltételezzük, hogy mindig a név második szavaként kaphatók meg.
Példa:

Toth Peter

9033 Gyor
Arpad ut 12.


     Kedves Peter, Eva,
     Istvan, Zsuzsa, Klara!

  Nagyon boldog karacsonyt es
  eredmenyekben gazdag uj evet kivanok

-------------------------------------------------------

Megoldás:
Tételezzük fel, hogy a címzettek személyi adatai egy

cimek:array[1..m] of szemelyi_adatok;

tömb első n elemében állnak rendelkezésünkre és deklarálva vannak még a következő változók is:

k,g,gy:integer;

Az üdvözlő sorokat előállító programrészlet:

...
for k:=1 to n do begin  { cimzettek ciklusa }
  { cimzes }
  writeln;writeln;
  writeln(cimek[k].neve);writeln;
  writeln(cimek[k].lakcime.iranyitoszam:4,' ',
          cimek[k].lakcime.helyseg);
  write  (cimek[k].lakcime.utca,' ',
          cime[k].lakcime.hazszam);
  if cimek[k].lakcime.emelet>=0 then begin  { emeletes haz }
    if cimek[k].lakcime.emelet=0 then write('fszt.')
    else write(cimek[k].lakcime.emelet:3,'. emelet');
    if cimek[k].lakcime.ajto>=0 then
      write(cimek[k].lakcime.ajto)
  end;
  writeln;writeln;writeln;

  { megszolitas }
  write(' ':10,'Kedves ');
  g:=1;
  repeat   { keresztnev keresese }
    g:=g+1
  until cimek[k].neve[g]=' ';
  g:=g+1;  { keresztnev kiirasa }
  while cimek[k].neve[g]<>' ' do begin
    write(cimek[k].neve[g]);
    g:=g+1
  end;
  if cimek[k].csaladi_allapot='h' then begin
    write(',');
    g:=1;
    repeat  { hazastars keresztnevenek keresese }
      g:=g+1
    until cimek[k].hazastars_neve[g]=' ';
    repeat
      write(cimek[k].hazastars_neve[g]);
      g:=g+1
    until cimek[k].hazastars_neve[g]=' '
  end;
  if (cimek[k].csaladi_allapot='h') or
     (cimek[k].csaladi_allapot='v') then
    if cimek[k].gyszam>0 then begin  { van gyerek }
      writeln(',');write(' ':10);
      for gy:=1 to cimek[k].gyszam do begin { gyerekek }
        g:=1;
        {a gyerekeknek csak a keresztnevet taroljuk }
        repeat
          write(cimek[k].gyerekek[gy,g]);
          g:=g+1
        until cimek[k].gyerekek[gy,g]=' ';
        if gy<cimek[k].gyszam then write(', ')
      end { gyerekek }
    end; { van gyerek }
  writeln('!');writeln;writeln;

  { udvozlo sorok }
  writeln('    Nagyon boldog karacsonyt es');
  writeln('    eredmenyekben gazdag uj evet kivanok');
  writeln;writeln;writeln;
  for g:=1 to 52 do write('-');
  writeln
end;
...

Tipikus hiba:
A változó részt is tartalmazó rekord egyes változatainak mezőire csak akkor szabad hivatkozni, ha az adott változat aktív. Így tehát a gyerekek számára vonatkozó if utasítást nem vonhatjuk össze az előtte álló feltétellel, mert ha a családi állapot mező értéke 'e' lenne, akkor a rekordnak nem volna gyszam mezője:

...
{ hibas! }
if ((cimek[k].csaladi_allapot='h') or
   (cimek[k].csaladi_allapot='v')) and
   (cimek[k].gyszam>0) then
...

A fenti megoldás egyszerűsített változatát, valamint további, rekordot is használó algoritmusokat tárgyalunk a with utasítás megismerése után.

3.9. A with utasítás

Az előbb látott programrészletnél elég kényelmetlen volt, hogy mindig ki kellett írni a cimek[k] hivatkozást, pedig a legkülső for ciklus magjában végig ennek a rekordnak az elemeivel foglalkoztunk. Amikor pedig a címet nyomtattuk ki, akkor a cimek[k].lakcíme rekord mezőire többször is hivatkoztunk, tehát ott a meglehetősen hosszú cimek[k].lakcime hivatkozást kellett újra és újra a programba írnunk.
Ez a kényelmetlenség elkerülhető a with utasítással, amely lehetővé teszi, hogy a benne megadott rekord, illetve rekordok mezőire úgy hivatkozzunk, hogy ne legyen szükséges minden alkalommal magát a rekordot is megadni.


A with utasítás

1. Alakja:

with utasítás:

rekordváltozó-lista:

2. A with utasítás a do után megadott egyetlen utasítás végrehajtását jelenti, de ebben az utasításban a with és do kulcsszavak között megadott rekordváltozó, illetve változók mezőnevei önállóan, azaz a rekord megadása nélkül is használhatók. (a) (b)

3. Egy rekordváltozó megadása a with utasítás rekordváltozó-listáján a rekord típusának megfelelő mezők nevének, mint önálló azonosítóknak a definiálási pontja, e nevek hatásköre a with utasítás további része, tehát a rekordváltozó-lista további elemei és a do utáni utasítás.(c) (d)

4. A rekordváltozó-lista egy eleméhez való hozzáférés a rekordváltozó-lista további elemeihez való hozzáférés és az utasítás végrehajtása előtt történik meg, ez a hozzáférés marad érvényben a with utasítás befejezéséig. (e)

5. A

with r1,r2...rn do utasítás

részlet azonos a következővel:

with r1 do
  with r2 do
    ...
      with rn do
        utasítás

azaz először az r1-hez való hozzáférés történik meg, és ennek mezői válnak önálló változókként hozzáférhetővé, azután az r2-höz való hozzáférés stb., végül az rn-hez való hozzáférés.

Magyarázatok, megjegyzések:

(a) Például a

var datum:record
        ev,ho,nap:integer
     end;

deklaráció esetén e rekord mezőértékei így is beolvashatók:

with datum do read (ev,ho,nap)

és ez teljesen azonos hatású a következővel:

read(datum.ev,datum.ho,datum.nap)

(b) Mivel a do kulcsszó után csak egyetlen utasítás állhat, ha itt több utasítást akarunk használni, azokat a begin...end kulcsszavakkal utasításcsoporttá kell összefogni.

(c) Az, hogy az így "megnyitott" rekord mezőnevei önállóan is használhatók a with utasításon belül, nem jelenti azt, hogy e mezők valóban önálló változókká váltak, például mezőnév továbbra sem lehet for utasítás ciklusváltozója.

(d) Vizsgáljuk meg a következő szerkezetet:

var g,k:integer;
     r:record
        a,b:real;
        j,g:char;
     end;
begin
   ...
   g:=3-k; { g es k onallo egesz valtozo }
   with r do begin
      a:=-1,85; { az a mezo kap erteket }
      k:=83; { k onallo valtozo kap erteket }
      g:=chr(k) { g itt az r rekord mezoje }
end;

Itt g használata az érdekes. A with előtt az önálló egész típusú változót érhetjük el, az r rekord mezőjét csak r.g formában, míg a with-en belül g a rekord mezőjét jelenti. Itt a g egész típusú változóra nem is tudunk hivatkozni, hiszen a g mezőnév mintegy eltakarja.

(e) Vegyük például a következő programrészleteket:

var g:integer;
     rt:array[1..100] of record
        a,b:real;
        c,k:char;
     end;
begin
   ...
   g:=...;
   with rt[g] do begin
      read(c,k);
      ...
      g:=g+1;
      a:=b*4.8
   end;

Itt az rt tömb rekordokból áll. Amikor belépünk a with utasításba, a gép g aktuális értékéből meghatározza, hogy melyik rekordról van szó, és a teljes with utasításban ennek a mezőihez lehet hozzáférni. Hiába változik meg közben g értéke, a következő utasításban az a és b mező a belépéskor érvényes g indexérték által kijelölt rekord eleme.


A 3.8.1. programrészlet with utasítás felhasználásával átalakított változata:

...
for k:=1 to n do begin  { cimzettek ciklusa }
  with cimek [k] do begin
    { cimzes }
    writeln;writeln;
    writeln(neve);writeln;
    with lakcime do begin
      writeln(iranyitoszam:4,' ',helyseg);
      write(utca,' ',hazszam);
      if emelet>=0 then begin  { emeletes haz }
        if emelet=0 then write('fszt.')
        else write(emelet:3,'. emelet');
        if ajto>=0 then write(ajto)
      end;
      writeln;writeln;writeln
    end { with lakcime };

    { megszolitas }
    write(' ':10,'Kedves ');
    g:=1;
    repeat   { keresztnev keresese }
      g:=g+1
    until neve[g]=' ';
    g:=g+1;  { keresztnev kiirasa }
    while neve[g]<>' ' do begin
      write(neve[g]);
      g:=g+1
    end;
    if csaladi_allapot='h' then begin
      write(',');
      g:=1;
      repeat  { hazastars keresztnevenek keresese }
        g:=g+1
      until hazastars_neve[g]=' ';
      repeat
        write(hazastars_neve[g]);
        g:=g+1
      until hazastars_neve[g]=' '
    end;
    if (csaladi_allapot='h') or (csaladi_allapot='v') then
      if gyszam>0 then begin  { van gyerek }
        writeln(',');write(' ':10);
        for gy:=1 to gyszam do begin { gyerekek }
          g:=1;
          {a gyerekeknek csak a keresztnevet taroljuk }
          repeat
            write(gyerekek[gy,g]);
            g:=g+1
          until gyerekek[gy,g]=' ';
          if gy<gyszam then write(', ')
        end { gyerekek }
      end; { van gyerek }
    writeln('!');writeln;writeln;
  end { with cimek };

  { udvozlo sorok }
  writeln('    Nagyon boldog karacsonyt es');
  writeln('    eredmenyekben gazdag uj evet kivanok');
  writeln;writeln;writeln;
  for g:=1 to 52 do write('-');
  writeln
end;
...

A with utasítás nemcsak a program szövegét tette rövidebbé, hanem általában a program futása is gyorsabbá válik, hiszen a gép a with utasításba való belépéskor meghatározhatja a rekord, esetünkben a cimek tömb k-adik elemének címét, így az egyes mezőkhöz való hozzáféréskor a címek meghatározásához már kevesebb számításra van szükség. Ebben a példában 24 helyen hivatkoztunk a cimek[k] rekord egyes elemeire, a programrészlet futása során pedig a belső ciklusok miatt ennél lényegesen több alkalommal történik rekordhivatkozás.

A with utasítás felvet egy olyan problémát, amivel eddig nem találkoztunk, mégpedig az azonosítók hatáskörének problémáját. A with utasítás bevezetése előtt használt azonosítóink a definíciós pontjuktól, tehát attól a helytől kezdve, ahol definiálva, illetve deklarálva voltak, a program végéig e definíció értelmében voltak használhatók. Ezzel szemben a with utasításban a mezőnevekre mint önálló azonosítókra csak a with utasítás végéig hivatkozhatunk, az előtt és után csak a szokásos módon, azaz rekordhivatkozás.mezőnév formában. Ha pedig létezett olyan önálló változó, amelynek a neve megegyezett valamelyik mezőnévvel, ez a with utasítás végéig elérhetetlenné vált, hiszen az ilyen azonosító a with utasításon belül a rekord megfelelő mezőjét jelentette (lásd: (d) megjegyzés a with szabályainál). Ez a szabály azt szolgálja, hogy a program szövegének minden pontján egyértelmű legyen az azonosítók jelentése.

Elképzelhető az az eset is, hogy egy with-en belül újabb with-et használunk egy ugyanolyan rekordra nézve. Ekkor a belső with-ben a mezőnevek az ezen belső with-ben megadott rekord mezőit jelentik, a külső with-ben megadott rekord mezőire pedig csak a rekord megadásával együtt lehet hivatkozni:

...
with cimek[1] do begin      { kulso with eleje }
   ... neve ... { cimek[1].neve }
   with cimek[2] do begin   { belso with eleje }
      ... neve ... { cimek[2].neve }
      ... cimek[1].neve ...    { cimek[1] rekord mezoje }
      with lakcime do begin  { with cimek[2].lakcime do }
         ... utca ...       { cimek[2].lakcime.utca }
      end;
      ...
   end;    { belso with vege }
   ... lakcime ...    { cimek[1].lakcime }
   with lakcime do
      ... utca ...     { cimek[1].lakcime.utca }
end;       { kulso with vege }

Itt tehát a belső with-ben is tudtunk hivatkozni a külső rekord elemeire. Más a helyzet az önálló váltókkal: ha egy mező azonosítója egy with utasításban ilyen változót "takar el", akkor nincs mód arra, hogy a with-en belül elérjük ezt a változót. Például ha létezik egy

gyszam:real;

deklarációjú változó, akkor a

with cimek[k] do begin
... gyszam ... { cimek[k].gyszam }
end; { with cimek[k] }

utasításon belül nincs mód arra, hogy a valós gyszam változónak értéket adjunk.

Tipikus hiba:
A with-re vonatkozó 4. szabály, illetve (e) magyarázat értelmében a következő programrészlet szinte biztosan végtelen ciklusra vezet:

read(g); { keresendo iranyitoszam }
k:=1;
with cimek[k] do
   while lakcíme.iranyitoszam<>g do
      k:=k+1;
...

Itt k értékét hiába növeljük meg, a ciklus teljes egészében a with utasításon belül van, tehát a feltételvizsgálat mindig a cimek[1] rekord mezőjét fogja g-hez hasonlítani. Így a ciklusból csak akkor lépünk ki, ha véletlenül már az első esetben egyezést találunk, vagy ha k értéke maxint-nél nagyobbá válna, ez azonban esetleg csak több órás futás után következik be, sőt az is előfordulhat, hogy ezt a túlcsordulást a gép nem is jelzi, tehát a program sohasem áll le.
Nagyon vigyázzunk tehát arra, hogy ciklusban feldolgozott rekordok esetén a with utasítás a ciklusmagban, tehát ne a cikluson kívül legyen.

3.9.1. Feladat:
Készítsen programrészletet, amely beolvassa az adatokat a cimek tömbbe!

3.9.2. Feladat:
Készítsen programrészletet, amely jókívánságokat nyomtat azoknak a nevére, akiknek a születésnapjai következő héten esedékes!

3.9.3. Feladat:
Készítsen programrészletet, amely jókívánságokat nyomtat azoknak a nevére, akiknek a névnapja a következő héten esedékes! A névnapok dátumait egy nevnap típusú elemekből álló tömbből keresse ki!

type szo=array[1..15] of char;
       nevnap=record
          keresztnev:szo; hónap:1..12;
          nap:1..31
       end;

Oldja meg a névnapok tárolására szolgáló tömb beolvasását is!

3.10. Táblázatkezelési módszerek

Programok írásakor nagyon gyakori feladat, hogy egy adatot adattáblázatból kell kikeresni, vagy a táblázat tartalmát kell módosítani, például új elemet kell hozzávenni vagy egy régit kell belőle kihagyni. Az ilyen jellegű feladatok megoldására sok módszer létezik. Most ezek közül a három legelterjedtebbet: a lineáris, a bináris vagy más néven logaritmikus és a csonkításos vagy keverő keresési módszert nézzük meg, mégpedig ugyanannak a feladatnak a megoldása kapcsán.

3.10.1. Példa:
Készítsünk programot telefonszámok nyilvántartására! Lehessen a nyilvántartásba új nevet és telefonszámot felvenni, a meglévők közül a név alapján a telefonszámot megkapni, az adott névhez tartozó bejegyzést törölni, illetve a nyilvántartás összes adatát kilistázni. Ne engedjük meg, hogy ugyanaz a név kétszer forduljon elő nyilvántartásunkban.

Megoldás:
A neveket és a telefonszámokat egy olyan táblázatban fogjuk tárolni, amely rekordokból áll, egy rekord a nevet és a névhez tartozó telefonszámot tartalmazza.
A program általános szerkezete mindhárom táblázatkezelési módszer esetén ugyanaz lesz.
A program indulásakor, mint a legtöbb programnál, itt is alaphelyzetbe kell hozni az adatokat, esetünkben ez azt jelenti, hogy a táblázat nem tartalmaz egyetlen bejegyzést sem.
A továbbiakban az egyes műveleteket a felhasználó parancsai szerint kell végezni, a lehetséges parancsok és a hozzájuk tartozó bemeneti adatok:

u név telefonszám új bejegyzés felvétele a telefonkönyvbe
k név   keresés; az adott névhez keressük a telefonszámot
t név   törlés; az adott bejegyzést megszüntetjük
l     listázás; kiírjuk az összes tárolt nevet és telefonszámot
v     vége; befejezzük a program használatát

A program általános szerkezete:

kezdeti beállítások,
parancsok ciklusa:
   parancs beolvasása,
   ha kell, további adatok beolvasása,
   parancs végrehajtása
amíg vége parancsot nem kapunk

Az u, k és t parancsok végrehajtásához először meg kell keresni az adott nevet a táblázatban. Ezt új elem felvétele előtt is el kell végezni, hiszen ha megtaláljuk a nevet, akkor hibaüzenetet kell kiírni, kétszer nem engedhetjük ugyanazt a nevet a táblázatba felvenni.
Az adatok beolvasását a HiSoft-Pascalban támogatott módszer szerint végezzük.

Lineáris keresés
Ez a legegyszerűbb, és a kereséshez szükséges műveletek számát tekintve leglassúbb módszer. A táblázatot a legkisebb indextől indulva úgy töltjük fel, hogy az elemeket, tehát a neveket és telefonszámokat a beérkezés sorrendjében írjuk a tömb következő szabad helyére, vagyis az eddig táblázatunkban szereplő elemek utáni első szabad helyre.
Kereséskor az 1-es indextől indulva egyenként vizsgáljuk meg a táblázat elemeit, akkor állunk meg, ha keresettel egyező nevet találunk, vagy ha a táblázat végére érünk; az előző esetben megtaláltuk a kívánt bejegyzést, az utóbbi esetben nem.
Ha egy elemet törölni akarunk, akkor helyére másoljuk a táblázatban szereplő utolsó nevet, és eggyel csökkentjük az utolsó elemet megadó indexet.

Ez a módszer onnan kapta a nevét, hogy egy elem megtalálásához átlagosan a táblázat feltöltött részének felét kell átvizsgálni, azaz a keresési lépések száma lineáris függvénye a tárolt elemek számának. Az első állítás akkor igaz, ha megtaláltuk a keresett adatot; ha nem, akkor az egész táblázatot át kell vizsgálni, tehát a lépések száma ekkor is lineáris függvénye a táblázatban tárolt elemek számának.

program telefonkonyv1;
const nevhossz=30;szamhossz=13;tablahossz=100;
type nevt=array[1..nevhossz] of char;
     szamt=array[1..szamhossz] of char;
var tabla:array[1..tablahossz] of record
        nev:nevt;
        telszam:szamt;
    end;
    parancs,ch:char;
    ujnev:nevt;   { keresendo nev }
    ujszam:szamt; { uj telefonszam }
    megvan:boolean;
    x,utolso,k:integer;
begin
{ kezdeti beallitasok }
  utolso:=0;  { utolso elem indexe }

  repeat
{ menu }
    writeln;
    writeln('''u'' - uj nev');writeln('''k'' - kereses');
    writeln('''t'' - torles');writeln('''l'' - listazas');
    writeln('''v'' - vege');
    writeln('Rogzitett nevek: ',utolso);write('Parancs: ');
    repeat
      read(parancs)
    until parancs in ['u','k','t','l','v'];
    if parancs in ['u','k','t'] then begin
      write('Nev: ');readln;read(ujnev)
    end;

    if parancs in ['u','k','t'] then begin
{ kereses }
      megvan:=false;x:=1;  { elso vizsgalt elem indexe }
      while (x<=utolso) and not megvan do begin
        megvan:=ujnev=tabla[x].nev;  { egyezik a nev? }
        if not megvan then x:=x+1    { ha nem, tovabblepes }
      end
    end;

    case parancs of  { a parancs vegrehajtasa }
{ uj elem felvitele }
    'u': if megvan then writeln('Van mar ilyen elem!')
        else
          if utolso=tablahossz then
            writeln('Megtelt a tablazat!')
          else begin
            write('Telefonszam: ');readln;read(ujszam);
            utolso:=utolso+1;
            with tabla[utolso] do begin
              nev:=ujnev;telszam:=ujszam
            end
          end;

{ telefonszam kiirasa }
    'k': if megvan then
          writeln('telefonszam: ',tabla[x].telszam)
          else writeln('Nincs ilyen nev a listan!');

{ telefonszam torlese }
    't': if megvan then begin
          tabla[x]:=tabla[utolso];
          utolso:=utolso-1 { csokken a feltoltott resz hossza }
         end
         else writeln('Nincs ilyen nev a listan!');

{ tablazat listazasa }
    'l': begin
          writeln('Lista:');
          for k:=1 to utolso do
            with tabla[k] do
              writeln(nev:nevhossz,': ',telszam);
          writeln
         end;

{ vege }
    'v': begin
          write('Valoban be akarja fejezni? (i/n) ');
          repeat
            read(ch)
          until (ch='i') or (ch='n');
          if ch='n' then parancs:=ch
         end
    end { case }
  until parancs='v'
end.

Tipikus hiba:
Vigyázat! Ha megpróbáljuk az előbbi keresési módszert egyszerűsíteni, könnyen elronthatjuk, ha a ciklus leállási feltételét naivan fogalmazzuk meg:

{ kereses }
x:=1; {elso vizsgalt elem indexe}
while (x<=utolso) and ujnev<>tabla[x].nev do
   x:=x+1;
megvan:=ujnev=tabla[x].nev; { egyezik a nev }

Ekkor ugyanis, ha nincs meg a keresett név, és utolso=tablahossz azaz tele van a táblázatunk, akkor a while feltételében végül x = utolso+1 = tablahossz+1 lesz, azaz kiindexeltünk a táblázatból a feltétel második rész-kifejezésével. Ez - mint már tudjuk - azért lehetséges, mert a Pascal nyelvben a gép akkor is kiértékelheti egy művelet - ebben az esetben az and logikai művet - mindkét operandusát, ha a művelet eredménye a másik operandus értéke miatt eldőlt már, mert itt pl. az hamis volt.
Érnek ellenére van lehetőségünk arra, hogy egyszerűbben fogalmazzuk meg a lineáris keresést:

{ kereses }
x:=1; { elso vizsgalt elem indexe }
while (x<utolso) and ujnev<>tabla[x].nev do
   x:=x+1;
megvan:=ujnev=tabla[x].nev; { egyezik a nev? }

Itt csak x értékének vizsgálata változott: az egyenlőséget már nem engedjük meg. Így ha nincs meg a táblázatban a keresett elem, akkor a while ciklus feltételének utolsó kiértékelésekor x=utolso és ekkor kilépünk a ciklusból, tehát biztosan nem indexelünk ki a tömbből. Természetesen a megvan változó értékét utólag, a ciklus után kell beállítanunk.

Lineáris keresés strázsával
Léleznek a lineáris keresésnél lényegesen jobb módszerek is, de mielőtt azokkal foglalkoznánk, érdemes megismerni az úgynevezett strázsa módszert, mely lineáris keresésnél felére csökkenti a kereséskor végzett összehasonlítások számát.
A keresésben ugyanis két dolgot kellett figyelni: megtaláltuk-e a keresett elemet, és a táblázat végére értük-e. Az utóbbi eset vizsgálatát megtakaríthatjuk, ha a keresés előtt a keresendő nevet betesszük a táblázat következő szabad helyére, így a keresési ciklusban nem kell vizsgálnunk, hogy a táblázat végére érünk már. Ekkor a keresés ennél az utolsó utáni helyre betett elemnél áll meg, ha előzőleg nem szerepelt a név a táblázatban, ha pedig igen, akkor már annál a bejegyzésnél. Természetesen a megtalálás tényét most nem az jelzi, hogy az x-edik elem neve azonos a keresettel, hanem az, hogy a keresés során átléptük a táblázat végét.
Ezzel a módszerrel az új elem felvétele is némileg egyszerűbbé válik, de megelőző vizsgálatnál, hogy megtelt-e a táblázat, csak tablahossz-1-ig lehet elmenni, mert a kereséshez mindig kell egy üres hely a táblázat végén:

program telefonkonyv2;
...
{ kereses }
    if parancs in ['u','k','t'] then begin
      tabla[utolso+1].nev:=ujnev;  { strazsa elhelyezese }
      x:=1; { elso vizsgalt elem indexe }
      while tabla[x].nev<>ujnev do
        x:=x+1; { tovabblepes }
      megvan:=x<=utolso;
    end;
...

{ uj elem felvitele }
      'u': if megvan then writeln('Van mar ilyen elem!')
         else
           if utolso=tablahossz-1 then
             writeln('Megtelt a tablazat!')
           else begin
             write('Telefonszam: ');readln;read(ujszam);
             utolso:=utolso+1;
             tabla[utolso].telszam:=ujszam
           end;
...

A többi művelet ugyanaz, mint az egyszerű lineáris keresésnél.
A strázsa tehát olyan elem egy adatszerkezetben, amely az adatszerkezet szélén megállítja a keresési ciklust anélkül, hogy külön figyelni kellene a szélekre. Ez a módszer több különböző adatszerkezet esetén is kényelmesen használható.

A strázsa fogalmának megismerése után érdemes megtanulni még egy fogalmat, ez pedig a kulcs.
A kulcs a különböző keresési feladatokban azt a mennyiséget, az adatoknak azt a részét jelenti, amelynek alapján keresünk, azaz amelynek segítségével eldöntjük, hogy az adott elem megfelel-e a keresés szempontjától. Ez az elem mintapéldáinkban a név volt. Lehetne azonban más mennyiség alapján is keresni. Például a születésnapi üdvözletek kiírásához a cimek táblázatban (3.9.2 feladat) a születési idő hónap és nap adatának kellett a megadott tartományba esnie, tehát ott az volt a kulcs.

Bináris, azaz logaritmikus keresés
Ez a módszer onnan kapta a nevét, hogy a kereséshez szükséges összehasonlítások számát a tárolt elemek számának kettes alapú logaritmusa adja meg, ami - főleg hosszabb táblázatok esetén - lényegeset kisebb, mint a lineáris keresésnél kapott érték.
A keresés meggyorsítása érdekében az elemeket itt a kulcsok szerint növekvő sorrendben tároljuk, azaz esetünkben a nevek ábécé sorrendjében. (Lehetne egyébként a kulcsok szerint csökkenő sorrendben is tárolni, ez nem változtat a módszer lényegén.)
Amikor egy elemet meg akarunk találni, akkor először a táblázat középső eleméhez hasonlítjuk; ha egyezik, akkor megtaláltuk, ha nem, akkor a táblázatnak abban a felében folytatjuk a keresést, amelyiket a középső elem és a keresett érték viszonya kijelöl: ha a keresett érték kisebb, mint a táblázat középső eleme akkor ez a táblázat első fele, ha nagyobb, akkor a második. Így az intervallum hossza, amelyben a keresést folytatjuk, minden lépésben feleződik. A keresést akkor fejezzük be, ha megtaláltuk a megfelelő elemet, vagy a keresési intervallum elfogy.
A módszer egyébként nagyon hasonló ahhoz, amit a 2.11.1. példában, a számkitalálós programnál láttunk (2.11. fejezet). Az is hasonló módon látható be, hogy a lépések száma a táblázatban tárolt elemek számának kettes alapú logaritmusa, felfelé kerekítve a következő egész értékre.

A keresés meggyorsításáért az új elem felvételének lassulásával fizetünk, hiszen a táblázat rendezettségét az új elem felvétele után is meg kell tartani, ehhez pedig az új elemet az eddigiek közé a nagyságának meg-felelő helyre kell illeszteni. Ez egy táblázat esetén csak úgy tehető meg, ha az új elemnél nagyobb kulcsú elemeket eggyel feljebb, azaz a magasabb indexek felé csúsztatjuk, tehát helyet csinálunk az új elemnek.
Átlagosan a táblázat elemeinek felét kell elcsúsztatnunk, és ez azt jelenti, hogy most az új elem felvételéhez szükséges lépések száma lett lineáris függvénye az elemek számának. Ezt a módszert tehát akkor érdemes használni, ha lényegesen gyakrabban keresünk a táblázatban, mint ahányszor bővítjük, ez telefonszámok esetén teljesül is.

program telefonkonyv3;
...
var ...
    also,felso:integer;

{ kereses }
    if parancs in ['u','k','t'] then begin
      also:=1;felso:=utolso;
      while (also<=felso) do begin
        x:=(also+felso) div 2;  { kozepso elem }
        if ujnev>=tabla[x].nev then also:=x+1;
        if ujnev<=tabla[x].nev then felso:=x-1
      end;
      megvan:=also-felso=2
    end;
...

{ uj elem felvitele }
      'u': if megvan then writeln('Van mar ilyen elem!')
         else
           if utolso=tablahossz then
             writeln('Megtelt a tablazat!')
           else begin
             write('Telefonszam: ');readln;read(ujszam);
             for k:=utolso downto also do  { helycsinalas }
               tabla[k+1]:=tabla[k];
             with tabla[also] do begin
               nev:=ujnev;telszam:=ujszam
             end;
             utolso:=utolso+1;
           end;
...

{ telefonszam torlese }
    't': if megvan then begin
          utolso:=utolso-1;
          for k:=x to utolso do  { lefele csusztatas }
            tabla[k]:=tabla[k+1]
         end
         else writeln('Nincs ilyen nev a listan!');
...

A kezdeti beállítás és a listázás nem változott a lineáris kereséshez képest, a keresés módszere viszont egy kicsit eltér az előzőektől. Ennek a változatnak az az előnye, hogy azt most nem kell figyelni a ciklusfeltételben, megtaláltuk-e a keresett elemet. Olyankor ugyanis a ciklusmagban mindkét feltétel teljesül, tehát a keresési intervallum határai megfordulnak: az alsó nagyobb lesz, mint a felső.
Ha a keresett elemet nem találtuk meg, akkor éppen az alsó határ mutat arra a helyre, ahová azt be kell illeszteni.

Csonkításos, avagy keverő keresés (hashing)
Ennél a módszernél nem a táblázat első elemeit használjuk a bejegyzések tárolására, hanem mintegy véletlenszerűen a táblázat tetszőleges elemeit. Ahhoz azonban, hogy vissza is tudjuk keresni az adatokat, méghozzá hatékonyan, igazából nem bízhatjuk a véletlenre, hogy egy adott elem hová kerül.
Foglalkozzunk először egy új elem felvételével! Választunk valamilyen módszert, mely az új elem kulcsához meghatároz egy egész értéket, mely az adatok tárolására használt tömb indextartományába esik - esetünkben az 1..tablahossz tartományba. Egyelőre nem érdekes, hogy milyen ez a módszer, csak az, hogy adott kulcshoz mindig ugyanazt az értéket rendelje. Nevezzük ezt az értéket elsődleges in-dexnek! A következő lépés az, hogy megnézzük, a táblázat ennyiedik eleme szabad-e. Ha igen, ide írjuk be az új elemet, és ezzel az új elem feldolgozása be is fejeződött. Ha nem szabad ez a hely, azt úgy nevezzük, hogy két elem ütközött, és valamilyen módon egy újabb indexet kell a bejegyzendő elem kulcsához rendelni. A legegyszerűbb módszer az, hogy az elsődleges indexnél eggyel nagyobb indexű helyet nézzük, ha ez is foglalt, akkor a következőt stb. Ha e lépegetés közben a táblázat végére érünk, akkora szabad hely keresését a táblázat elején folytatjuk. Ha volt még egyáltalán szabad hely, akkor ezzel a módszerrel előbb-utóbb megtaláljuk.
A keresés módszere ezek után könnyen kitalálható: a keresendő elem kulcsához ugyanazzal a módszerrel meghatározzuk az elsődleges indexet, mint az új elem felvételekor, majd ettől a helytől indulva addig lé-pegetünk előre, amíg meg nem találjuk a keresett elemet vagy egy üres táblázatelemet, az utóbbi azt jelzi, hogy nincs a táblázatban a keresett elem. Ha új elemet akarunk felvenni, akkor ez a keresés végén kapott, első üres helyre mutató index megadja, hogy az új elemet hová kell felvenni.
A módszer alkalmazásához az is szükséges, hogy minden táblázatelemről külön-külön meg lehessen álla-pítani, üres-e. Ezt úgy érhetjük el, hogy a kezdeti beállítások során a táblázat minden kulcsába olyan értéket írunk, ami nem szerepelhet tényleges adatként - a mi esetünkben például a név első karakterébe szóközt teszünk.
A legbonyolultabbnak egy elem törlése tűnik, hiszen nem tehetjük meg, hogy a törlendő bejegyzést egy-szerűen szabaddá tesszük. Ekkor ugyanis, ha van a táblázatban olyan elem, amelynek elsődleges indexe az így törölt elemre, vagy az elé mutatott, de ütközések miatt végül a törölt elem utánra került, azt az elsődleges index és a jelenlegi hely közé beékelődött üres hely miatt nem találjuk meg egészen addig, amíg erre az üres helyre ismét egy érvényes bejegyzés nem kerül. Ezért a törlést úgy csináljuk, hogy olyan értéket írunk a törlendő bejegyzés kulcsába, amelyet a régi elemek keresése érvényesnek, de a keresendő elem kulcsától különbözőnek, az új elem felvételekor végzett keresés pedig üresnek tekint. Legyen esetünkben ez a név első karakterébe tett * (csillag) karakter!
Ilyen törlési módszer esetén azonban más lesz a régi elem kereséséhez és más az új elem felvételéhez használt keresés, hiszen az utóbbinak a törölt elemeket is üresnek kell tekintenie és nem kell figyelnie a kulcsok egyezését.

Foglalkozzunk most az elsődleges indexet meghatározó módszerrel! Világos, hogy olyan módszer lenne jó, amely az elemeket a táblázatban a lehető legegyenletesebben osztja el, így az ütközések valószínűsége a legkisebb. Sok különböző módszer kidolgozható, az egyik például az, hogy a kulcsot egyetlen egész számnak tekintjük, osztjuk a táblázat hosszával és vesszük a maradéknál eggyel nagyobb értéket:

kulcs mod tablahossz+1

Ez valóban az 1...tablahossz tartományba esik, és az esetek nagy részében elég egyenletes eloszlású. Az előbbi maradékképzés eredménye úgy is tekinthető, mintha a kulcsot tablahossz alapszámú számrendszerben írtuk volna fel, és az elsődleges kulcshoz csak az utolsó számjegyet hagytuk volna meg. Innen származtatható a csonkításos keresés elnevezés.
A gyakorlatban olyan módszert kell választani az elsődleges index meghatározására, ami könnyű és gyors, Az előbbi módszer nem ilyen, hiszen nagyon nagy egész számmal kellene hozzá maradékképzést végezni, Ezért olyan eljárást választunk, amely valamilyen más módszerrel keveri ki a kulcsból az elsődleges indexet.

program telefonkonyv4;
...
var ...
    x0:integer;

{ kezdeti beallitasok }
  utolso:=0;  { utolso elem indexe }
  for k:=1 to tablahossz do
    tabla[k].nev[1]:=' ';
...

{ kereses }
      if parancs in ['u','k','t'] then begin
        x0:=0; { elsodleges index meghatarozasa }
        for k:=1 to nevhossz do
          x0:=(x0+ord(ujnev[k])*k) mod tablahossz+1;
        x:=x0; { kereses elsodleges indextol }
        while (tabla[x].nev<>ujnev) and (tabla[x].nev[1]<>' ') do
          x:=x mod tablahossz+1;
        megvan:=ujnev=tabla[x].nev;
      end;
...
{ uj elem felvitele }
    'u': if megvan then writeln('Van mar ilyen elem!')
        else
          if utolso=tablahossz then
            writeln('Megtelt a tablazat!')
          else begin
            write('Telefonszam: ');readln;read(ujszam);
            x:=x0;
            while (tabla[x].nev[1]<>' ') and
                  (tabla[x].nev[1]<>'*') do
              x:=x mod tablahossz+1;
            with tabla[x] do begin
              nev:=ujnev;telszam:=ujszam
            end;
            utolso:=utolso+1
          end;
...
{ telefonszam torlese }
    't': if megvan then begin
          tabla[x].nev[1]:='*';  { ervenytelen kulcs }
          utolso:=utolso-1;  { foglalt eleme szama }
         end
         else writeln('Nincs ilyen nev a listan!');
...
{ tablazat listazasa }
    'l': begin
          writeln('Lista:');
          for k:=1 to tablahossz do
            with tabla[k] do
              if(nev[1]<>' ') and (nev[1]<>'*') then
                writeln(nev,': ',telszam);
          writeln
         end;

Az előbbi módszer hatékonyságának megállapítása nehezebb feladat, mint a korábbiaké, de azonnal látható, hogy a véletlen keresés akkor igazán jó, ha a táblázatnak csak kis százalékát töltik ki adatok, sűrűn feltöltött táblázat esetén ugyanis a módszer lineáris kereséssé válik. Ha a táblázat méretét legalább a tárolt elemek számának kétszeresére választjuk, akkor az esetek túlnyomó többségében két lépés elég lesz akár a keresésre, akár az új elem felvételére.
Megjegyzendő, hogy a módszer hatékonyságát nemcsak az elsődleges index meghatározásának módja, hanem ütközések esetén a következő index megválasztása is befolyásolja. Az általunk használt módszer ebből a szempontból nem éppen a legszerencsésebb, mert a táblázatban összefüggő foglalt tartományok alakulnak ki, és ezek annál gyorsabban nőnek, minél hosszabbak, tehát a táblázat telítettségének növekedésével a hatékonyság erősen romlik.

Akit mélyebben is érdekelnek a táblázatkezelési módszerek, azoknak a [12] irodalmat ajánlom, különösem a 3. kötetet.

3.10.1. Feladat:
Egészítse ki az előbbi algoritmusokat, illetve keretprogramot úgy, hogy lehetővé váljon nevek visszakeresése telefonszám alapján is.

3.10.2. Feladat:
Elemezze az alábbi bináris keresési algoritmust! Mi az előnye, ill. hátránya az előbbi változathoz képest?

also:=1;felso:=utolso; { kereses kezdeti hatarai }
while also<=felso do begin
  x:=(also+felso) div 2; { kozepso elem }
  if ujnev>=tabla[x].nev then also:=x+1
  else felso:=x-1
end;
x:=(also+felso) div 2;
if x<=utolso then
  if ujnev>tabla[x].nev then x:=x+1;
megvan:=false;also:=x;
if x<=utolso then megvan:=tabla[x].nev=ujnev;

3.10.3. Feladat:
Találjon ki további módszereket a csonkításos kereséshez az elsődleges index kiszámítására és az ütközéskor alkalmazandó továbblépési módszerre!

3.10.4. Feladat:
Készítsen programot, amely meghatározza és kirajzolja a táblázat feltöltöttségének függvényében, hogy az egyes módszerek esetén milyen gyakorisággal fordul elő ütközés az elsődleges indexnél, illetve a továbblépések során!

3.11. Változók deklarálása

A változó fogalmával már az első programunkban megismerkedtünk, de deklarálásuk és használatuk pontos szabályait csak most, az adattípusokra vonatkozó általános szabályok megismerése után érdemes tárgyalni.


Változódeklaráció

1. A változó olyan nyelvi elem, amelyben értéket lehet tárolni, és ezt az értéket meg lehet változtatni.

2. A deklaráció alakja:

változódeklaráció:

3. Az azonosítók listáján felsorolt elemek mind különböző változókat jelentenek, ezek típusa azonos, a típusmegadás szerinti lesz.

4. A változó használata a változóhoz való hozzáférést jelenti a használat időpontjában. Egy változó mindig a neki utoljára adott értéket, az úgynevezett aktuális értéket tárolja. Ez megváltoztatható értékadó utasítással, read, readln, get, put eljárással, illetve új értéket még eljárás vagy függvény aktuális változóparamétereként kaphat. (a)

5. A változóhoz való hozzáférés a változó típusától függően önálló változóhoz, változó eleméhez, mutatóval elért változóhoz, illetve bufferváltozóhoz való hozzáférést jelent.

6. A változó értéke meghatározatlan, ha létrejötte óta nem kapott értéket, vagy valamilyen művelet során elvesztette az értékét. Változó az értékét dispose eljárás használata, változó részt tartalmazó rekordban az előzőtől eltérő változat aktívvá válása, ill. adatállomány bufferváltozója az előre deklarált put vagy get eljárás használata miatt veszítheti el. (b)

Magyarázatok, megjegyzések:

(a) A változóhoz való hozzáférés időpontja, azaz hogy mikor adunk a változónak értéket, illetve mikor hivatkozunk a változó segítségével tárolt értékre, nagyon fontos jellemzője a hozzáférésnek, hiszen a változónak éppen az a legfontosabb jellemzője, hogy értéke a program végrehajtása során többször is módosítható, sőt lehet meghatározatlan is.

(b) A legtöbb megvalósítás (így sem a Turbo Pascal, sem a HiSoft-Pascal) nem jelez hibát, ha olyan változó értékét használjuk fel számításban vagy egyéb műveletben, amelynek az értéke meghatározatlan. A művelet eredménye azonban biztosan értelmetlen lesz, ezért ezt feltétlenül el kell kerülni.

3.12. Pakolt adattípusok

Az eddigiek során egyetlen pakolt adattípussal találkoztunk, ez karakterláncok gazdaságos tárolását tette lehetővé. A packed kulcsszót azonban nemcsak karakterekből álló tömb, hanem tetszőleges más összetett adattípus előtt is megadhatjuk.


Pakolt, azaz packed típusok

1. Pakolt adattípus megadása:

pakolt típus:

packed pakolatlan_összetett_típus

ahol pakolatlan_összetett_típus: tömb típus, rekord típus, halmaz típus vagy adatállomány típus

2. A packed kulcsszó megjelenése az összetett adattípus megadásában a processzor számára azt a lehetőséget jelzi, hogy az adatok tárolóbeli helyfoglalását gazdaságosabban, tömörebben oldhatja meg, akár az így tárolt adatokhoz való hozzáférés időigényének vagy a hozzáférést végző utasítások helyigényének a növekedése árán is. A packed szó pontos hatása megvalósításfüggő. (A packed kulcsszó a Turbo Pascalban, és a HiSoft-Pascalban hatástalan.)

3. Pakolt összetett változó elemeivel ugyanazok a műveletek végezhetők, mint pakolatlan elemeivel, kivéve, hogy pakolt összetett változó eleme nem lehet aktuális változóparaméter.

4. A packed kulcsszó csak az adott összetett adattípusra vonatkozik, tehát ha a pakolt összetett adattípus elemei is összetettek, de maguk nem pakoltak, azok tárolási módját nem befolyásolja.

5. Azonos elemtípusú pakolt és pakolatlan tömbök tartalma átvihető egymásba a pack illetve unpack előre deklarált eljárások segítségével. (E két eljárás sem a Turbo Pascalban, sem a HiSoft-Pascalban nincs implementálva.)

Feltételezzük a következő változódeklarációkat:

pakolatlan:array[pn1..pn2] of elem;
pakolt:packed array[pa1..pa2] of elem;

A pack(pakolatlan,k,pakolt) utasítás jelentése:

begin
   g:=k;
   for m:=pa1 to pa2 do begin
      pakolt[m]:=pakolatlan[g];
      if m<>pa2 then g:=succ(g);
   end;
end

Az unpack(pakolt,pakolatlan,k) utasítás jelentése:

begin
   g:=k;
   for m:=pa1 to pa2 do begin
      pakolatlan[g]:=pakolt[m];
      if m<>pa2 then g:=succ(g);
   end;
end

ahol g, k és m megfelelő típusú segédváltozók, melyek nem fordulnak elő a program többi részében.


Tovább a 4-7. fejezethez

Vissza az Enterprise könyvekhez
Vissza a Spectrum mkönyvekhez