Programarea Calculatoarelor. Limbajul Pascal

  • Uploaded by: Cătălin
  • 0
  • 0
  • May 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Programarea Calculatoarelor. Limbajul Pascal as PDF for free.

More details

  • Words: 59,740
  • Pages: 221
Cuprins

1. Tipuri dinamice de date. Lucrul cu adrese ......................................................

3

2. Structura de listă în limbajul Pascal ................................................................

15

3. Grafuri. Implementări în limbajul Pascal ........................................................

28

4. Structuri arborescente ......................................................................................

48

5. Algoritmi recursivi. Metodele divide et impera şi backtracking ............

73

6. Reprezentarea vizuală a datelor .......................................................................

92

7. Tehnici speciale în Pascal ....................................................................................

123

8. Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri ..................................................................................................

143

9. Unele aspecte tehnice referitoare la prelucrarea fişierelor ....................

162

10. Obiecte în Pascal .................................................................................................

174

Anexa 1 – Unitatea DOS ..........................................................................................

208

Anexa 2 – Unitatea CRT ..........................................................................................

215

Anexa 3 – Erori de execuţie ...................................................................................

217

Bibliografie .................................................................................................................

221

TIPURI DINAMICE DE DATE LUCRUL CU ADRESE

Datele de tip static au caracteristici care limitează rezolvarea unor clase de probleme. În primul rând, spaţiul de memorie aferent unor astfel de date se defineşte şi se rezervă la dimensiune maximă, prestabilită, ca spaţiu propriu care nu poate fi disponibilizat şi nici împărţit cu alte date, chiar dacă, în momentul diverselor execuţii ale programului, nu este în întregime utilizat (rezervare statică sau la momentul compilării). În al doilea rând, componentele structurilor statice ocupă locuri prestabilite în spaţiul rezervat, determinate de relaţia de ordonare specifică fiecărei structuri. În al treilea rând, limbajul defineşte operaţiile admise cu valorile componentelor, potrivit tipului de bază al structurii, astfel încât numărul maxim şi ordinea componentelor structurii nu pot fi modificate. În aceste condiţii, structurile statice sunt dificil de utilizat în rezolvarea problemelor care prelucrează mulţimi de date pentru care numărul şi ordinea componentelor se modifică frecvent în timpul execuţiei programului. Pentru astfel de situaţii, limbajul PASCAL oferă posibilitatea utilizării datelor de tip dinamic, cărora li se pot aloca şi elibera zone de memorie pe parcursul execuţiei programului.

1.1 Lucrul cu adrese în Pascal Adresarea memoriei se realizează prin registre ale unităţii centrale, care au capacitatea de un cuvânt. La adresarea în modul real, pentru formarea unei adrese fizice din spaţiul de 1Mo este necesară folosirea a două registre: de segment (segment), care conţine adresa de început a segmentului, numită şi adresa de bază; de deplasare (offset), care precizează distanţa la care se află octetul adresat faţă de începutul segmentului. Astfel, orice adresă din memorie poate fi specificată în formatul segment:offset sau, în alţi termeni, bază:deplasare. Întrucât deplasarea de 16 biţi nu poate accesa o locaţie de memorie din afara domeniului 0..216-1, rezultă că dimensiunea maximă a unui segment este de 64 Ko, restricţie valabilă pentru orice produs utilizat sub MS-DOS. Memoria este împărţită în paragrafe de câte 16 octeţi, iar fiecare segment începe la graniţă de paragraf, adică de la o adresă divizibilă cu 16. Într-un spaţiu de 1Mo sunt 216 paragrafe, ceea ce înseamnă că adresa de început a unui segment, corespunzând unui număr de

3

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

paragraf, poate fi reprezentată ca o valoare pe 16 biţi. În calculul adresei fizice pe 20 biţi, se aplică următoarea relaţie: segment*16+offset, unde segment şi offset desemnează conţinutul registrelor de segment (un număr de paragraf), respectiv de deplasare. Adresele din segmentul curent se numesc apropiate (near), iar cele din afara acestuia sunt îndepărtate (far). Accesul la o adresă apropiată presupune doar schimbarea conţinutului registrului de deplasare, în timp ce pentru o adresă îndepărtată trebuie schimbată atât valoarea registrului de segment, cât şi a celui de deplasare. În unitatea System sunt definite funcţiile Seg(Var x):WORD, Ofs(VAR x):WORD care furnizează adresa de segment şi deplasarea variabilei, procedurii sau funcţiei x. În Pascal există tipul de date pointer, memorat pe două cuvinte, în care cuvântul superior (high) conţine partea de segment, iar cuvântul inferior (low) pe cea de deplasare asociate unei adrese. Pentru a se putea exemplifica modul de lucru cu adrese, se precizează faptul că: • tipul pointer se defineşte prin construcţia de forma ^tip; • adresarea indirectă a unei variabilei se defineşte prin construcţia identificator^; • referirea adresei unei variabile se defineşte prin construcţia @identificator. Programatorul trebuie să facă distincţie între adresa şi conţinutul unei variabile, precum şi între adresarea directă şi cea indirectă. În exemplul de mai jos, liniile sursă numerotate de la 1 la 8 se generează următoarele tipuri de adresare: 1, 2, 4:adresare directă pentru ambii operanzi şi lucru cu conţinut; 3: adresare directă pentru pa şi a, lucru cu conţinut pentru pa şi cu adresa a; 5: adresare directă pentru c şi indirectă pentru pa, lucru cu conţinut; 6, 7, 8: adresare indirectă şi lucru cu conţinut. De remarcat că pa este de tip pointer, pa^ este de tip REAL iar @a este adresa a (are configuraţie de pointer). În sintaxa @identificator, identificator se referă la o variabilă, procedură sau funcţie. Efectul referirii @identificator este similar celui obţinut prin funcţia Addr definită în unitatea System astfel: Addr(VAR x):pointer, unde x este variabilă, funcţie sau procedură. Folosirea referirii identificator^ presupune existenţa unei adrese valide în variabila identificator. Exemplu: 1.1. VAR

a,b,c:REAL; pa,pb:^REAL; BEGIN a:=20; b:=a; pa:=@a; pb:=pa; c:=pa^;

{Se rezervă câte 6 octeţi pentru fiecare variabilă} {Se rezervă câte 4 octeţi pentru fiecare variabilă} {Se atribuie valoarea 20 variabilei de adresa a} {Se atribuie variabilei de adresa b, conţinutul variabilei de adresa a} {Se atribuie variabilei de adresa pa, adresa a}

{Se atribuie variabilei de adresa pb, conţinutul variabilei de adresa pa} {Se atribuie variabilei c, conţinutul variabilei a cărei adresă este memorată în pa; aceasta este adresare indirectă prin pa. Lui c i se atribuie conţinutul lui a (20)}

1 2 3 4 5

4

Tipuri dinamice de date. Lucrul cu adrese WriteLn( 'Valoarea ',pb^:10:2,');

{Se scrie continutul variabilei a carei adresa este în pb } WriteLn( 'Adresa fizica a lui A :

6

,seg(pb^),':',ofs(pb^));

{Se scrie adresa a, sub forma segment:deplasare}

7

WriteLn( 'Adresa fizica a lui PB:',seg(pb),':',ofs(pb));

Œ

{Se scrie adresa pb, sub forma segment:deplasare}

8

1.2 Structura memoriei la execuţia unui program După încărcarea programului executabil, memoria aferentă lui se structurează în următoarele regiuni (segmente): segmentul prefix program, regiunea de cod, segmentul de date, stiva şi zona heap (figura 1.1). Pentru adresarea acestora, unitatea centrală de prelucrare foloseşte registre specializate (tabelul 1.1), la unele dintre ele existând acces direct (vezi tipul Registers din unitatea DOS, anexa 1) sau indirect din programe Pascal. Tabelul 1.1 Registre de segment /deplasare şi funcţii standard asociate Tipul segmentului Segment de cod Segment de date Segment de stivă

Registrul de segment CS (CSeg) DS (DSeg) SS (SSeg)

Registrul de deplasare IP SI SP (SPtr)

Seg:Ofs CS:IP DS:SI SS:SP (SSeg:SPtr)

Observaţie: Cseg, DSeg, SSeg, SPtr sunt funcţii de tip WORD, nu au parametri şi sunt definite în unitatea System. • Segmentul prefix al programului (PSP) este o zonă de 256 de octeţi constituită de MS-DOS la încărcarea în memorie a fişierului de tip .EXE. El conţine informaţii necesare sistemului de operare pentru a transfera controlul către program, respectiv pentru a returna controlul către sistemul de operare la încheierea execuţiei acestuia. Adresa de segment este memorată în variabila publică PrefixSeg, de tip WORD, definită în unitatea System. • Regiunea de cod este constituită din mai multe segmente de cod: unul corespunzând programului principal, respectiv câte unul pentru fiecare unitate referită în program. Primul segment de cod este cel asociat programului principal, urmat de cele ale unităţilor, în ordinea inversă specificărilor din clauza USES. Ultimul segment de cod, introdus implicit în orice program executabil, corespunde unităţii System, care conţine biblioteca de subprograme standard referite la momentul execuţiei (Run-time library). În absenţa clauzei USES, zona de cod va conţine două segmente: cel al programului principal şi cel al unităţii System. Codul poate fi împărţit într-un număr oarecare de segmente, singura limitare fiind dată de memoria disponibilă. Registrul CS

5

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

conţine adresa de start a instrucţiunilor programului, iar registrul IP (registru pointer al instrucţiunilor) precizează adresa următoarei instrucţiuni de executat. Programele Pascal nu au acces la registrul IP, dar valoarea curentă a registrului CS poate fi obţinută cu funcţia CSeg. Adresa de început a zonei de cod este CSeg:0000. Conţinutul registrului CS se poate modifica pe parcursul execuţiei, depinzând de faptul că instrucţiunile executate sunt din programul principal sau din una dintre unităţi. (Limita superioară a memoriei RAM convenţionale) Zona neutilizată în heap Heap

HeapPtr

Variabile dinamice curent alocate în heap

HeapOrg SSeg:SPtr

Stiva

Zona neutilizată în stiva

SSeg:0000 Date DSeg:0000

Cod

CSeg:0000 PrefixSeg:0000

Date înregistrate în stiva

Variabile globale Constante cu tip Unitatea System Unităţi specificate în clauza USES Exemplu: USES Crt,A,B,C;

Segment cod Crt Segment cod A Segment cod B

Segment cod C Segment cod program principal Program Segment Prefix (PSP)

Conţinut fişier .EXE

Fig. 1.1 Harta memoriei la execuţia unui program Pascal • Segmentul de date este unic şi conţine constantele cu tip urmate de variabilele globale. Atunci când necesarul de memorie pentru datele interne depăşeşte 64Ko, trebuie să se recurgă la folosirea unor tehnici adecvate (memorarea datelor în heap sau pe medii externe, folosirea compactării etc.). Registrul DS conţine adresa de început a segmentului de date şi nu se modifică pe parcursului execuţiei. SI reprezintă registrul index al sursei, folosit pentru a puncta (a indexa) o dată în cadrul segmentului de date. Pentru instrucţiunile asupra şirurilor de caractere, registrul SI punctează pe operandul sursă, în timp ce un alt registru, index al destinaţiei (DI), punctează operandul destinaţie. Funcţia DSeg returnează valoarea curentă a registrului DS. Registrele SI şi DI pot fi accesate indirect, printr-un apel la o întrerupere. Adresa de început a segmentului de date este DSeg:0000.

6

Tipuri dinamice de date. Lucrul cu adrese

• Segmentul de stivă, ca şi cel de date, poate avea maximum 64Ko, reducându-se la unul singur. Stiva este folosită în lucrul cu subprograme pentru memorarea parametrilor formali, variabilelor locale şi adreselor de revenire. Registrul SS conţine adresa de început a stivei şi nu se modifică pe parcursul execuţiei. Registrul pointer al stivei (SP) precizează deplasarea curentă în cadrul stivei. Funcţia SSeg returnează valoarea registrului SS, iar SPtr returnează valoarea curentă a registrului SP. În cadrul stivei, alocarea spaţiului se face începând de la adrese mai mari spre adrese mai mici. Adresa curentă este definită de SS:SP sau, conform celor precizate anterior, de SSeg:SPtr. • Zona variabilelor dinamice poate corespunde întregii memorii convenţionale a calculatorului, rămasă disponibilă după încărcarea programului. În heap se memorează variabilele dinamice, buffer-ele pentru structuri de reacoperire şi pentru lucrul în modul grafic. Adresa de început a zonei heap este dată de variabila publică HeapOrg, iar adresa curentă este dată de variabila HeapPtr, ambele de tip pointer, definite în unitatea System. Alocarea variabilelor începe de la adrese mai mici către adrese mai mari, spaţiul maxim ce poate fi alocat unei variabile neputând depăşi 64Ko (strict riguros, 65520 octeţi), ca urmare a limitărilor impuse mecanismului de adresare a memoriei. După modul lor de funcţionare, stiva şi heap-ul pot fi asimilate cu două stive aşezate spate în spate. Programatorul poate controla repartizarea memoriei disponibile între stivă şi heap în faza de execuţie cu directiva de compilare {$M}, care are următoarea formă sintactică: {$M StackSize,HeapMin,HeapMax} StackSize trebuie să fie un întreg din domeniul 1024 (1Ko) la 65520 (64 Ko), prin care se specifică mărimea segmentului de stivă. HeapMin şi HeapMax specifică dimensiunea minimă, respectiv maximă a heap-ului, teoretic cu valori între 0 şi 640 Ko. Riguros, HeapMin poate avea valori de la 0 la 655360, iar HeapMax trebuie să fie în domeniul de la HeapMin la 655360. Valorile implicite pentru aceşti parametri de alocare sunt {$M 16384,0,655360}. Rezultă că dimensiunea implicită a stivei este de 16 Ko, iar zona de heap se extinde, teoretic, în tot spaţiul rămas liber în memoria convenţională. Practic, din dimensiunea de 640 Ko trebuie scăzut, pe lângă spaţiul ocupat de programul însuşi, cel corespunzător componentelor sistemului de operare rezidente în memorie pe parcursul execuţiei.

1.3 Tipuri dinamice de date În Pascal se operează cu două tipuri de date dinamice - referinţă şi pointer primele fiind "cu tip" iar celelalte "fără tip". • Tipul referinţă are sintaxa: tip_referinţă=^tip_de_bază;. Simbolul ^ are semnificaţia de "indirect". Datorită asocierii cu un tip de bază, variabilele tip_referinţă se mai numesc şi variabile cu referinţă legată. La compilare, pentru astfel de variabile, se vor rezerva în segmentul de date două cuvinte şi la referirea lor se vor genera

7

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

instrucţiuni cod maşină conform tipului de bază, dar cu adresare indirectă. Înainte de referire, în variabilele de tip_referinţă trebuie să se încarce adresele variabilelor de tipul tip_de_bază. Declararea unui tip referinţă permite referirea anterior declarării tipului de bază. Astfel, următoarea secvenţă de declarări este corectă: TYPE pointer_a=^vector; vector=ARRAY[1..20] OF REAL; Construcţia sintactică a referirii unei variabile dinamice depinde de caracteristicile tipului său de bază: este de forma identificator^ în cazul tipurilor nestructurate sau celor structurate care permit referirea globală (STRING, RECORD şi SET); conţine prefixul identificator^, urmat de elementele specifice modului de referire a componentelor, în cazul tipurilor structurate care permit referirea pe componente (ARRAY, STRING şi RECORD). Aceste posibilităţi de referire sunt ilustrate în programul Alocare_dinamică_1. • Tipul pointer este desemnat prin cuvântul rezervat pointer. Variabilele de tip pointer pot fi denumite variabile cu referinţă liberă, deoarece pot fi folosite la memorarea adreselor pentru variabile de orice tip. Tehnica de lucru cu astfel de variabile este asemănătoare celei prezentate la tipul referinţă. Utilizarea efectivă presupune şi în acest caz o asociere explicită cu un anumit tip de bază, dar soluţia folosită este diferită. La tipul referinţă, asocierea se face prin declarare, iar în cazul tipului pointer asocierea se realizează la utilizare, prin diverse tehnici. O posibilitate este asigurată de referinţa typecasting (transfer de tip), care are forma generală: tip(variabilă), unde tip este un tip standard sau declarat anterior de utilizator iar variabilă poate fi cu/fără tip sau o referinţă prin pointer, de forma variabilă_pointer^. Din punct de vedere fizic, variabilele de tip referinţă_legată şi pointer memorează adrese sub forma segment:offset. De aceea, în limbajul curent de specialitate, ambele tipuri se definesc prin termenul pointer. Următorul exemplu evaluează expresia e:=a+b, folosind adresarea indirectă pentru toate variabilele (a şi e prin referinţă_cu_tip iar b prin pointer): VAR a,b,e:REAL; pa,pe:^REAL; pb:POINTER; BEGIN pa:=addr(a); pb:=@b; pe:=@e; Write(‘A=); ReadLn(pa^); Write(‘B=’); ReadLn(REAL(pb^)); pe^:=pa^+REAL(pb^); WriteLn(‘E= ‘,pe^:8:2)) END.

Variabilele pointer (referinţă sau pointer) pot fi puse în relaţie cu operatorii = şi < >. Două variabile vor fi egale dacă au componentele segment, respectiv offset egale. De remarcat faptul că două variabile de tip pointer care indică aceeaşi adresă pot fi neegale, deoarece le diferă componentele. Variabilele pointer pot fi folosite în atribuiri. Atât în relaţii cât şi în atribuiri sunt definite următoarele clase de compatibilitate: tipul referinţă este compatibil cu orice alt tip dinamic; două variabile de tip referinţă sunt compatibile dacă sunt de acelaşi tip. Observaţie: pa şi pb nu sunt de acelaşi tip dacă sunt declarate astfel: pa:^real; pb:^real.

8

Tipuri dinamice de date. Lucrul cu adrese

Ele sunt de acelaşi tip dacă sunt declarate astfel: pa,pb:^real. Este definită o constantă simbolică (nil) cu semnificaţie de valoare nulă a tipului dinamic (valoarea nil nu punctează o zonă de memorie).

1.4 Utilizarea zonei heap Noţiunea de dinamic este strâns legată de utilizarea zonei de memorie heap (deşi tipul dinamic poate fi asociat variabilelor memorate în orice componentă a memoriei principale). În unitatea System sunt definite următoarele variabile de tip pointer, care pot fi folosite în gestionarea zonei heap: HeapOrg, HeapPtr, HeapEnd, HeapError, FreeList. HeapOrg punctează pe adresa de început a zonei heap, iar HeapEnd dă adresa de sfârşit a heap-ului. HeapPtr conţine următoarea adresa disponibilă din heap. Ea este variabila prin care se gestionează fiecare nouă alocare, punctând pe prima adresă disponibilă din heap. Toate procedurile de alocare (New, GetMem, Mark) lucrează cu această variabilă. HeapError corespunde adresei rutinei de tratare a erorilor de alocare pentru variabile dinamice. FreeList serveşte la gestiunea blocurilor devenite libere în interiorul heap-ului, punctând pe primul bloc liber în heap, care punctează pe al doilea ş.a.m.d. Ultimul bloc liber punctează pe vârful heap-ului, adică pe locaţia dată de HeapPtr, asigurându-se astfel posibilitatea realocării acestor spaţii. Dacă în interiorul heap-ului nu există blocuri libere, atunci FreeList va fi egală cu HeapPtr. De asemenea, în unitatea System sunt definite o serie de proceduri şi funcţii care pot fi utilizate în lucrul cu variabilele dinamice. Procedurile GetMem(p,n), FreeMem(p,n), respectiv New(p) şi Dispose(p) se folosesc pentru a aloca/elibera un bloc a cărui adresă este dată de variabila pointer sau referinţă, p. Deoarece zona heap este limitată, iar alocarea şi eliberarea dinamică determină alternanţa unor zone libere cu cele ocupate, este necesară cunoaşterea spaţiului disponibil şi a spaţiului contiguu maxim disponibil. În acest scop pot fi folosite funcţiile (fără argumente, cu rezultat de tip LONGINT) MemAvail (pentru spaţiul total disponibil) şi MaxAvail (pentru spaţiul contiguu maxim disponibil). Iniţial, rezultatul furnizat de MemAvail corespunde dimensiunii totale a heap-ului, care poate fi obţinută şi prin aplicarea formulei (Seg(HeapEnd^)-Seg(HeapOrg^))*16, întrucât adresele de început şi de sfârşit ale heap-ului sunt exprimate ca numere de paragraf. De asemenea, această dimensiune coincide iniţial cu cea furnizată de funcţia MaxAvail, care precizează cel mai lung bloc de locaţii de memorie contigue disponibile în heap. Se poate determina dacă spaţiul disponibil este contiguu, pe baza expresiei relaţionale MemAvail = MaxAvail. Dimensiunea în octeţi ocupată de o variabilă poate fi obţinută cu funcţia SizeOf(VAR x):WORD, unde x este identificator de variabilă. Se poate stabili dacă spaţiul disponibil este acoperitor pentru o variabilă de un anumit tip, scriind o relaţie de forma MaxAvail >= SizeOf (tip). • Alocarea şi eliberarea zonelor pentru variabile referinţă_legată se face cu

9

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

procedurile New, respectiv Dispose, definite în unitatea System astfel: New(VAR p:pointer), Dispose(VAR p:pointer). Procedura New rezervă în heap o zonă de memorie de lungime egală cu cea indicată de tipul de bază şi încarcă în variabila p adresa acestei zone. Exemplu: 1.2. VAR px:^INTEGER; {La compilare se rezerva 4 octeti in segmentul de date} ………………………………… BEGIN ………………………………… New(px); {La executie se rezerva 2 octeti si se incarca adresa zonei

rezervate in px} px^:=21; {Se memoreaza valoarea 21 in zona de tip INTEGER din heap} Dispose(px); {Se elibereaza spatiul rezervat in heap} De remarcat că există două niveluri de rezervare: statică (corespunzătoare lui px) şi dinamică (datorată procedurii New(px), care rezervă variabilă în heap, în conformitate cu tipul de bază). Din punct de vedere fizic, operaţia realizează memorarea în variabila p a valorii HeapPtr şi avansarea acesteia din urmă cu lungimea specifică tipului de bază. Dacă nu există o zonă contiguă disponibilă de lungime mai mare sau egală cu cea necesară, se generează eroare de execuţie. Procedura Dispose eliberează zona alocată variabilei. Următorea alocare se poate face pe spaţiul eliberat, dacă este acoperitor ca lungime. Fizic, se reface în HeapPtr valoarea de dinaintea alocării prin New. În programul Alocare_dinamică_1 se ilustrează prin exemple simple alocarea şi eliberarea spaţiului din heap, precum şi referirile globale şi pe componente pentru variabile dinamice ale căror tipuri de bază sunt nestructurate sau structurate. La execuţia programului, pe lângă afişarea mesajelor care ilustrează corectitudinea folosirii tehnicii de lucru cu variabile dinamice, se remarcă revenirea în final, după eliberarea tuturor zonelor alocate, la dimensiunea iniţială a heap-ului. Prin posibilitatea de generare a unor succesiuni de valori de acelaşi tip, de la simpla alocare dinamică de spaţiu pentru o singură valoare a unei variabile se poate trece la realizarea unor structuri dinamice de date. Acest lucru este posibil pe baza înlănţuirii succesiunilor de valori, ca urmare a includerii la fiecare element al structurii a două părţi: o parte de informaţii, corespunzând valorii propriu-zise a elementului; o parte de legătură, care va conţine adresa următorului element. Fără a se urmări, în acest context, abordarea problematicii implementării structurilor de date complexe (liste, stive, cozi, arbori binari etc.), în programul Alocare_dinamică_2 se prezintă realizarea unei liste înlănţuite pentru memorarea în heap a unui vector. Aplicaţia are în vedere memorarea în heap a unui vector de mari dimensiuni, cu elemente de tip întreg. Pentru fiecare element se construieşte o structură de tip RECORD, în forma asociată tipului element. Pentru simplificare, se cere utilizatorului să precizeze numărul total de valori şi valoarea de start, elementele fiind generate automat, ca numere întregi consecutive. După construire se reia traversarea

10

Tipuri dinamice de date. Lucrul cu adrese

listei, cu afişarea pe monitor a elementelor vectorului. Prin procedura New(leg) se va aloca o zonă de memorie de dimensiunea unei date de tipul element, iar în variabila de tip leg se va memora adresa de început a acestei zone. Zona de memorie a cărei adresă este conţinută în variabila de tip leg va fi referită prin indirectare, folosind o construcţie sintactică formată din - sau începând cu variabila de tip leg^. PROGRAM Alocare_dinamica_1; USES Crt; TYPE pv=^v; v=ARRAY[1..100] OF INTEGER; ps=^s; s=STRING[120]; pm=^m; m=SET OF CHAR; pc=^CHAR; pa=^a; a=RECORD nume:STRING[15]; nota:ARRAY[1..5] OF 1..10; end; VAR legv:pv; legs:ps; legm:pm; legc:pc; lega:pa; BEGIN ClrScr; WriteLn('Adresa de inceput heap: ', Seg(HeapOrg^),':',Ofs(HeapOrg^)); Writeln('Adresa de sfirsit heap: ', Seg(HeapEnd^),':',Ofs(HeapEnd^)); WriteLn('Valoare pointer heap: ', Seg(HeapPtr^),':',Ofs(HeapPtr^)); WriteLn('Dimensiune totala heap: ',MemAvail); {Rezultat echivalent: (Seg(HeapEnd^)-Seg(HeapOrg^))*16 } WriteLn('Bloc maxim in heap: ',MaxAvail); New(legv); WriteLn('Memorie alocata in heap: ',SizeOf(legv^)); WriteLn('Memorie libera in heap: ',MemAvail); legv^[1]:=1; WriteLn('v[1]=',legv^[1]); Dispose(legv); WriteLn('Memorie libera in heap: ',MemAvail); New(legs); legs^:='PASCAL'; WriteLn('Al treilea caracter din sir este ',legs^[3]); New(legm); legm^:=['a'..'c']; IF 'c' IN legm^ THEN Writeln('Litera "c" apartine multimii'); New(legc); legc^:=#65; Writeln('Caracter atribuit: ',legc^); New(lega); lega^.nume:='POPESCU ION'; lega^.nota[2]:=10; WriteLn('Studentul ',lega^.nume,' are nota ',lega^.nota[2], ' la disciplina a doua'); Dispose(lega); Dispose(legs); Dispose(legc); Dispose(legm); WriteLn('Memorie libera in heap: ',MemAvail); END.

Din analiza programului Alocare_dinamică_2 se pot desprinde câteva din cerinţele impuse programatorului în construirea şi prelucrarea structurilor dinamice. Astfel, pentru a face posibilă traversarea ulterioară, se memorează în variabila de tip referinţă inceput adresa primului element. La construirea unui nou element, inclusiv a primului, câmpul de legătură urm (de tip referinţă) este iniţializat cu valoarea nil; la

11

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

traversarea ulterioară, regăsirea unei valori nil corespunde identificării ultimului element din listă. Pentru toate elementele, începând cu al doilea, construirea presupune revenirea la elementul anterior, a cărui informaţie de legătură trebuie să puncteze pe elementul nou construit. Această balansare este asigurată prin “jocul” variabilelor de tip referinţă curent, respectiv următor. PROGRAM Alocare_dinamica_2; leg=^element; element=RECORD v:INTEGER; urm:leg; END; VAR inceput,curent,urmator: leg; n,vi,i:INTEGER; BEGIN Write('Nr. de elemente si valoare de inceput: ReadLn(n,vi); New(inceput); inceput^.v:=vi; inceput^.urm:=nil; WriteLn('S-a creat elementul 1 = ',inceput^.v); curent:=inceput; FOR i:=2 TO n DO BEGIN New(urmator); urmator^.v:=curent^.v+1; urmator^.urm:=nil; WriteLn('S-a creat elementul ',i,' = ',urmator^.v); curent^.urm:=urmator; curent:=urmator END; i:=1; curent:=inceput; WHILE curent^.urm <> nil DO BEGIN WriteLn('v[',i,']: ',curent^.v); curent:=curent^.urm; Inc(i) END; Writeln('v[',i,']: ',curent^.v) END.

');

• Alocarea şi eliberarea zonelor pentru variabile de tip pointer se realizează cu procedurile GetMem, respectiv FreeMem, definite în unitatea System astfel: GetMem(VAR p:pointer; l:WORD), FreeMem(VAR p:pointer; l:WORD). Efectul acestora este asemănător procedurilor New şi Dispose, cu precizarea că este necesară specificarea lungimii, care nu poate fi dedusă implicit, neexistând un tip de bază. Un exemplu simplu de folosire a variabilelor pointer din heap este ilustrat în programul Alocare_dinamică_3. PROGRAM Alocare_dinamica_3;

12

Tipuri dinamice de date. Lucrul cu adrese VAR p: POINTER; BEGIN GetMem(p,6); REAL(p^):=5.25; WriteLn(REAL(p^):4:2); FreeMem(p,6) END.

Reluând exemplul propus prin programul Alocare_dinamică_2, folosirea variabilelor pointer presupune unele adaptări, ilustrate în Alocare_dinamică_4. Din program se constată că aplicarea tehnicii de permite atât referirea globală, cât şi pe componente, ilustrată în acest câmpurile unui articol. Spaţiul alocat la un apel al procedurii GetMem dimensiunii articolului (6 octeţi).

trecerea la programul typecasting caz pentru corespunde

PROGRAM Alocare_dinamica_4; TYPE element=RECORD v:INTEGER; urm:POINTER; END; VAR inceput,curent,urmator: POINTER; n,vi,i : INTEGER; BEGIN Write('Nr. de elemente si valoare de inceput: '); ReadLn(n,vi); GetMem(inceput,6); element(inceput^).v:=vi; element(inceput^).urm:=nil; WriteLn('S-a creat elementul 1 = ',element(inceput^).v); curent:=inceput; FOR i:=2 TO n DO BEGIN GetMem(urmator,6); element(urmator^).v:=element(curent^).v+1; element(urmator^).urm:=nil; WriteLn('S-a creat elementul ',i,' = 'element(urmator^).v); element(curent^).urm:=urmator; curent:=urmator END; i:=1; curent:=inceput; WHILE element(curent^).urm <> nil DO BEGIN WriteLn('v[',i,']: ',element(curent^).v); curent:=element(curent^).urm; Inc(i) END; Writeln('v[',i,']: ',element(curent^).v) END.

• Ca alternative ale procedurilor New, GetMem, respectiv Dispose, FreeMem, pot fi folosite procedurile Mark şi Release, definite în unitatea System astfel: Mark(VAR p:pointer), Release(VAR p:pointer). Procedura Mark memorează în variabila p valoarea din HeapPtr, iar procedura Release depune în

13

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

variabila HeapPtr conţinutul variabilei p. Folosirea în pereche a procedurilor Mark şi Release oferă posibilitatea ca, după diverse alocări, să se restabilească valoarea variabilei HeapPtr cu valoarea memorată prin Mark. Apelul Release(HeapOrg) aduce HeapPtr pe începutul zonei heap (eliberează zona). În exemplul următor, se memorează în y adresa de la un moment dat a lui HeapPtr (fie ea a), se modifică HeapPtr prin rezervarea a 12 octeţi (pentru x2, x3), se reface conţinutul lui HeapPtr cu adresa a, ceea ce înseamnă că x3^ se rezervă la această adresă (în locul variabilei x2^). VAR x1,x2,x3.x4:^REAL; y:POINTER; BEGIN New(x1); x1^:12; Mark(y); {Memorarea valorii HeapPtr in Y} New(x2); x2^:=10; New(x3); x3^:=34; Release(y); {Reincarcarea valorii din Y in Heapptr} New(x4); x4:=46; {Se va memora peste valoare 10 din x2} ………………………

14

STRUCTURA DE LISTĂ ÎN LIMBAJUL PASCAL

Unul din principalele atribute ale datelor este structura sau modul de organizare care le caracterizează. Structurile rezultă prin gruparea într-un anumit mod a colecţiilor de date primitive. Exemple de structuri de date sunt: tablourile, mulţimile, înregistrările. În continuare vor fi prezentate noi tipuri de organizări, şi anume: listele simplu şi dublu înlănţuite şi cazurile particulare cunoscute sub numele de stive şi cozi.

2.1 Noţiuni introductive Un tip de structură de date poate fi gândit logic, făcând abstracţie de modul particular în care datele sunt reprezentate în memoria calculatorului. El se reprezintă particularizat prin diverse implementări din limbajele de programare. Specificarea unui anume tip de organizare pentru datele procesate de un program presupune definirea mulţimii de valori permise pe care o variabilă sau parametru aparţinând acestui tip de date le poate avea, precum şi a mulţimii de operaţii care pot fi efectuate cu astfel de date. Eficienţa unei scheme de prelucrare a datelor este determinată în mare măsură de tipurile particulare de date utilizate. Limbajul de programare Pascal oferă suficientă flexibilitate pentru reprezentarea unor tipuri complexe de date, dar controlul corectitudinii modului în care sunt utilizate revine programatorului. Structurile de date Pascal pentru reprezentarea colecţiilor de date pot fi clasificate în structuri statice şi dinamice. Structurile statice cuprind: array, set şi file pentru reprezentarea datelor de acelaşi tip (omogene) şi record, pentru reprezentarea datelor eterogene. Structurile dinamice se construiesc pe baza tipurilor de date Pascal referinţă legată şi referinţă liberă (desemnată prin tipul predefinit pointer). Definirea unei date reprezentată printr-o structură statică presupune alocarea unui spaţiu de memorie de dimensiune invariabilă pe durata evoluţiei programului, în timp ce dimensiunea unei date reprezentată prin intermediul unei structuri dinamice poate fi modificată pe durata execuţiei.

15

Programarea calculatoarelor - Tehnica programării în limbajul Pascal

Pentru anumite clase de probleme, structurile statice de date nu numai că nu sunt suficiente, dar se dovedesc chiar imposibil de folosit, datorită limitărilor la care sunt supuse. În primul rând, spaţiul de memorie aferent unor astfel de date se defineşte şi se rezervă în momentul compilării programului (rezervare statică), la o dimensiune maximă (cuprinzătoare). Acest spaţiu nu poate fi disponibilizat şi nici împărţit cu alte date, chiar dacă nu este în întregime utilizat în anumite momente ale execuţiei programului. În al doilea rând, componentele structurilor statice ocupă locuri prestabilite în spaţiul rezervat, determinate de relaţia de ordonare specifică fiecărei structuri. În al treilea rând, limbajul defineşte operaţiile admise cu valorile componentelor, potrivit tipului de bază al structurii, astfel încât numărul maxim şi ordinea componentelor structurii nu pot fi modificate. În aceste condiţii, structurile statice sunt dificil de utilizat în rezolvarea problemelor care prelucrează mulţimi de date pentru care numărul şi ordinea componentelor se modifică frecvent în timpul execuţiei programului. Pentru astfel de situaţii, există posibilitatea utilizării datelor de tip dinamic, cărora li se pot aloca şi elibera zone de memorie pe parcursul execuţiei programului.

2.2 Structura de listă Organizarea de tip listă corespunde unei structurări lineare a datelor, la nivelul fiecărei componente dispunându-se de informaţie suficientă pentru identificarea următoarei componente a colecţiei. Datele dintr-o colecţie astfel structurată sunt referite de obicei prin termenii de noduri, celule, componente etc. Reprezentarea unei liste în limbajul Pascal poate fi realizată prin intermediul structurii de date array, ordinea componentelor fiind dată de ordinea pe domeniul valorilor corespunzătoare indexării şi, în consecinţă, următoarea componentă este implicit specificată. Memorarea unei mulţimi de date {d1, d2,…, dn} prin intermediul unei structuri statice poate fi realizată în limbajul Pascal utilizând un masiv unidimensional. Definirea tipului de date Pascal pentru memorarea ca listă a datelor {d1, d2,…, dn} este: const max=500; type lst=array[1..max] of tip_informatie; unde n ≤ max şi tip_informatie este numele tipului de date Pascal utilizat pentru memorarea fiecărei date din mulţimea {d1, d2,…, dn}. Dacă lista este variabila de tip lst utilizată pentru memorarea colecţiei {d1, d2,…, dn}, atunci data di este memorată în componenta lista[i], 1 ≤ i ≤ n .

16

Structura de listă în limbajul Pascal

Dezavantajele majore în utilizarea reprezentării statice rezidă în volumul de calcule necesare efectuării operaţiilor de inserţie/eliminare de noduri şi în necesitatea păstrării unei zone de memorie alocată, indiferent de lungimea efectivă a listei. Aceste dezavantaje sunt eliminate prin opţiunea de utilizare a structurilor dinamice. Componentele unei liste dinamice sunt eterogene, fiecare nod conţinând o parte de informaţie şi câmpuri de legătură care permit identificarea celulelor vecine. Câmpurile de legătură sunt reprezentate de date de tip referinţă (adresă). În cazul listelor cu un singur câmp de legătură (simplu înlănţuite), valoarea câmpului indică adresa nodului următor, în timp ce, în cazul listelor cu dublă legătură (dublu înlănţuite), valorile memorate în câmpurile de legătură sunt adresele componentelor care preced şi, respectiv, urmează celulei. În ambele situaţii, câmpul de legătură pentru indicarea celulei următoare corespunzător ultimei componente a listei are valoarea nil în cazul listelor “deschise” (lineare) şi respectiv indică adresa primei componente din listă în cazul listelor “închise” (circulare). Se presupune că mulţimea de date {d1, d2,…, dn} este memorată ca listă lineară. Convenţional, structura rezultată poate fi reprezentată grafic fie prin a) (listă simplu înlănţuită), fie prin b) (listă dublu înlănţuită), unde simbolul desemnează valoarea nil a câmpului de legătură (figura 2.1). În cazul în care pentru memorarea datelor {d1, d2,…, dn} se doreşte utilizarea unei structuri de listă circulară, reprezentarea grafică este dată în figura 2.2. dn

dn-1



d1

d2

a)

C C dn

dn-1



d2

d1

b)

Fig. 2.1 Liste lineare Declarările tipurilor de date Pascal pentru definirea structurilor de liste dinamice simplu şi, respectiv, dublu înlănţuite sunt: b) Listă dublu înlănţuită a) Listă simplu înlănţuită type lista=^nod; type lista=^nod; nod=record nod=record inf:tip_informatie; inf:tip_informatie; leg: lista; st,dr: lista; end; end; unde tip_informatie este numele tipului de date Pascal utilizat pentru memorarea fiecărei date din mulţimea{d1, d2,…, dn}.

17

Programarea calculatoarelor - Tehnica programării în limbajul Pascal

dn



dn-1

d1

d2

ultim

a)

dn

dn-1



d2

b)

d1

ultim

Fig. 2.2 Liste circulare

2.3 Operaţii primitive asupra listelor Accesul la informaţia stocată într-o variabilă de tip listă revine efectuând una sau mai multe dintre operaţiile primitive: regăsirea nodului (dacă există) care corespunde unei chei date (condiţie impusă asupra valorii câmpului de informaţie), inserarea unei noi componente în listă, eliminarea componentei (componentelor) cu proprietatea că valorile câmpurilor de informaţie satisfac o anumită cerinţă şi înlocuirea câmpului de informaţie corespunzător unei componente printr-o informaţie dată. Accesarea componentelor unei liste reprezentată printr-o structură statică poate fi realizată atât secvenţial, cât şi direct, utilizând valorile indicelui considerat pentru indexare, în timp ce accesarea componentelor unei liste dinamice se realizează, de regulă, numai secvenţial, începând cu prima componentă şi continuând cu următoarele, pe baza valorilor câmpurilor de legătură. Convenţional, numim cap al listei dinamice pointerul a cărui valoare este adresa primei componente a listei. 1. Parcurgerea integrală a datelor memorate într-o listă. Se presupune că declarările de tip pentru definirea structurilor de liste menţionate anterior sunt globale, relativ la procedurile descrise în continuare.

18

Structura de listă în limbajul Pascal

a) Lista reprezentată prin structură statică (masiv unidimensional) procedure parcurgere1(var l:lst; n:word); var i:word; begin for i:=1 to n do prelucrare(l[i]); end;

b) Lista reprezentată prin structură dinamică simplu înlănţuită procedure parcurgere2( var cap:lista); var p:lista; begin p:=cap; while(p<>nil) do begin prelucrare(p^.inf); p:=p^.leg; end; end;

c) Lista reprezentată prin structură dinamică dublu înlănţuită procedure parcurgere3( var cap:lista); var p:lista; begin p:=cap; while(p<>nil) do begin prelucrare(p^.inf); p:=p^.dr; end; end;

2. Regăsirea unei date d într-o colecţie memorată într-o listă În continuare se presupune că funcţia egal(a,b:tip_informatie):boolean returnează valoarea true dacă a şi b coincid, altfel returnează false. Funcţiile Pascal exista1, exista2 şi exista3 calculează valoarea true, dacă data d se află în colecţia memorată în lista dată ca argument, altfel calculează valoarea false.

a) Lista reprezentată prin structură statică (masiv unidimensional) function exista1(l:lst; n:word; d:tip_informatie):boolean; var i:word; c:boolean; begin c:=false; i:=1; while(i<=n)and(not c) do if egal(d,l[i]) then c:=true else i:=i+1; exista1:=c; end;

b) Lista reprezentată prin structură dinamică simplu înlănţuită function exista2(cap:lista;d:tip_informatie):boolean; var p:lista;

19

Programarea calculatoarelor - Tehnica programării în limbajul Pascal c:boolean; begin p:=cap; c:=false; while(p<>nil)and(not c) do if egal(d,p^.inf) then c:=true else p:=p^.leg; exista2:=c; end;

c) Lista reprezentată prin structură dinamică dublu înlănţuită function exista3(cap:lista;d:tip_informatie):boolean; var p:lista; c:boolean; begin p:=cap; c:=false; while(p<>nil)and(not c) do if egal(d,p^.inf) then c:=true else p:=p^.dr; exista3:=c; end;

3. Inserarea unei date, d, într-o listă Includerea unei noi componente într-o listă poate fi realizată, în funcţie de cerinţele problemei particulare, la începutul listei, după ultima componentă din listă, înaintea/după o componentă cu proprietatea că valoarea câmpului de informaţie îndeplineşte o anumită condiţie. Presupunem că funcţia condiţie ((a,b:tip_informatie):boolean calculează valoarea true dacă a şi b verifică condiţia formulată pentru inserare, altfel calculează valoarea false. De asemenea, procedura atribuie(var destinatie:tip_informatie; sursa:tip_informatie) realizează copierea datei sursa în destinatie. Deoarece prin inserarea unei componente se poate ajunge la depăşirea spaţiului disponibil de memorie, este necesară verificarea prealabilă a posibilităţii realizării operaţiei (dacă se poate aloca spaţiu de memorie pentru componenta de inserat). În cazul listelor reprezentate prin structuri statice, această verificare va fi realizată prin compararea lungimii efective n a listei cu dimensiunea declarată max. În cazul listelor dinamice, este utilizată funcţia Pascal predefinită MaxAvail, care calculează lungimea maximă a zonelor nealocate contigue din memoria Heap. Valoarea returnată pentru parametrul test este true, dacă inserarea a fost posibilă, altfel este false. De exemplu, inserarea la începutul listei decurge astfel: a) Lista reprezentată prin structură statică (masiv unidimensional) procedure inserare_la_inceput1(var d:tip_informatie;var test:boolean);

l:lst;

var

n:word;

20

Structura de listă în limbajul Pascal var i:word; begin test:=true; if n=max then test:=false else begin for i:=n downto 1 do atribuie(l[i+1],l[i]); atribuie(l[1],d); n:=n+1; end; end;

b) Lista reprezentată prin structură dinamică simplu înlănţuită procedure inserare_la_inceput2(var cap:lista;d:tip_informatie;var test:boolean); var p:lista; begin test:=true; if MaxAvail<sizeof(nod) then test:=false else begin new(p); atribuie(p^.inf,d); p^.leg:=cap; cap:=p; end; end;

c) Lista reprezentată prin structură dinamică dublu înlănţuită procedure inserare_la_inceput3(var cap:lista;d:tip_informatie;var test:boolean); var p:lista; begin test:=true; if MaxAvail<sizeof(nod) then test:=false else begin new(p); atribuie(p^.inf,d); p^.dr:=cap;p^.st:=nil; cap:=p; end; end;

Procedurile de inserare la începutul listei pot fi folosite şi pentru crearea unei liste în care să fie memorată o colecţie de date {d1, d2,…, dn}. Procedura copiaza(var d:tip_informatie; var i:word) realizează copierea în d a datei di din colecţia considerată. Utilizând procedurile de inserare deja descrise, operaţia de creare a unei liste poate fi realizată astfel: a) Lista reprezentată prin structură statică (masiv unidimensional) procedure creeaza1(var l:lst; n:word; var test:boolean); var i,j:word;

21

Programarea calculatoarelor - Tehnica programării în limbajul Pascal d:tip_informatie; begin test:=true; if n>max then test:=false else begin i:=1; while(i<=n) do begin copiaza(d,i); inserare_la_inceput1(l,i,d,test) end; end; end;

b) Lista reprezentată prin structură dinamică simplu înlănţuită procedure creeaza2(var cap:lista;n:word;var test:boolean); var i:word; begin i:=0; cap:=nil; test:=true; while(i
4. Eliminarea (ştergerea) unei date, d, dintr-o listă Modificarea conţinutului unei liste prin eliminarea uneia sau mai multor componente poate fi descrisă secvenţial, astfel încât este suficient să dispunem de o procedură care realizează eliminarea unei singure componente. Criteriile de eliminare pot fi formulate diferit, cele mai uzuale fiind: prima componentă, ultima componentă, prima componentă care îndeplineşte o anumită condiţie, respectiv componenta ce precede/urmează prima componentă care îndeplineşte o condiţie dată. În aceste cazuri este necesară verificarea existenţei în lista considerată a componentei ce trebuie eliminată. Verificarea asigură şi testarea faptului că lista prelucrată este vidă sau nu. Analog operaţiilor de inserare, în cadrul procedurilor următoare, parametrul test returnează valoarea true, dacă eliminarea este efectivă, altfel returnează false. De exemplu, eliminarea primei componente dintr-o listă are loc astfel: a) Lista reprezentată prin structură statică (masiv unidimensional) procedure elimina_prima1(var l:lst; d:tip_informatie;var test:boolean); var i:word; begin test:=true; if n=0 then test:=false else begin atribuie(d,l[1]);

var

n:word;

var

22

Structura de listă în limbajul Pascal for i:=1 to n-1 do atribuie(l[i],l[i+1]); n:=n-1; end; end;

b) Lista reprezentată prin structură dinamică simplu înlănţuită procedure elimina_prima2(var cap:lista;var d:tip_informatie;var test:boolean); var p:lista; begin test:=true; if cap=nil then test:=false else begin p:=cap; cap:=cap^.leg; atribuie(d,p^.inf); dispose(p); end; end;

c) Lista reprezentată prin structură dinamică dublu înlănţuită procedure elimina_prima3(var cap:lista;var d:tip_informatie;var test:boolean); var p:lista; begin test:=true; if cap=nil then test:=false else begin p:=cap; cap:=cap^.dr; cap^.st:=nil; atribuie(d,p^.inf); dispose(p); end; end;

2.4 Liste circulare În anumite cazuri, este preferabilă renunţarea la structura de tip linear a listelor şi utilizarea unei legături de la ultima componentă către capul listei, rezultând ceea ce se numeşte listă circulară (figura 2.2). Avantajul utilizării acestui tip de structură este posibilitatea de accesare dintr-un element al listei a oricărui alt element. În continuare sunt prezentate module pentru realizarea unor operaţii de bază în lucrul cu liste circulare. type tip_informatie=string; clista=^nod; nod=record inf:tip_informatie; leg:clista; end;

23

Programarea calculatoarelor - Tehnica programării în limbajul Pascal procedure atribuie(var d:tip_informatie;s:tip_informatie); begin d:=s; end; procedure inserare_la_inceput(var ultim:clista;d:tip_informatie;var test:boolean); var p:clista; cap:clista; begin test:=true; if MaxAvail<sizeof(nod) then test:=false else begin new(p); atribuie(p^.inf,d); if ultim=nil then begin ultim:=p; ultim^.leg:=ultim; end else begin cap:=ultim^.leg; p^.leg:=cap; ultim^.leg:=p; end; end; end; procedure inserare_la_sfarsit(var ultim:clista;d:tip_informatie;var test:boolean); var p:clista; cap:clista; begin test:=true; if MaxAvail<sizeof(nod) then test:=false else begin new(p); atribuie(p^.inf,d); if ultim=nil then begin ultim:=p; ultim^.leg:=ultim; end else begin cap:=ultim^.leg; p^.leg:=cap; ultim^.leg:=p; ultim:=p; end; end; end; procedure elimina_la_inceput(var ultim:clista;var d:tip_informatie;var test:boolean); var cap:clista;

24

Structura de listă în limbajul Pascal begin test:=true; if ultim=nil then test:=false else begin cap:=ultim^.leg; atribuie(d,cap^.inf); if cap=ultim then begin dispose(ultim); ultim:=nil; end else begin ultim^.leg:=cap^.leg; dispose(cap); end; end; end; procedure elimina_la_sfarsit(var ultim:clista;var d:tip_informatie;var test:boolean); var p:clista; cap:clista; begin test:=true; if ultim=nil then test:=false else begin cap:=ultim^.leg; atribuie(d,ultim^.inf); if cap=ultim then begin dispose(ultim); ultim:=nil; end else begin p:=cap; while(p^.leg<>ultim) do p:=p^.leg; p^.leg:=cap; dispose(ultim); ultim:=p end; end; end; procedure afisare(ultim:clista); var p,cap:clista; begin if ultim=nil then writeln('Lista vida') else begin cap:=ultim^.leg; p:=cap; repeat write(p^.inf,' '); p:=p^.leg; until p=cap; writeln; end; end;

25

Programarea calculatoarelor - Tehnica programării în limbajul Pascal

2.5 Stive şi cozi Accesul la informaţia memorată într-o listă lineară pentru efectuarea operaţiilor de inserare şi eliminare este permis la oricare dintre componentele colecţiei. Aşa după cum a rezultat în §2.3, identificarea poziţiei în care trebuie efectuată inserarea/eliminarea presupune iniţierea unei secvenţe de operaţii de căutare, ceea ce determină creşterea timpului de lucru. Pe de altă parte, o mulţime de aplicaţii pot fi modelate utilizând liste lineare în care introducerea şi, respectiv, eliminarea informaţiilor sunt permise numai la capete. Astfel au fost definite tipurile de listă stivă şi coadă, care impun un tip de organizare a aplicării operaţiilor de inserare şi eliminare. Stiva Se numeşte stivă o listă organizată astfel încât operaţiile de inserare şi eliminare sunt permise numai la prima componentă. Acest mod de organizare corespunde unei gestiuni LIFO (Last In First Out) a informaţiei stocate. Modelul corespunde unei stive de cărţi. Adăugarea unei noi cărţi în stivă se face deasupra primei cărţi, iar extragerea este posibilă numai pentru prima carte. Operaţiile de inserare şi eliminare într-o stivă pot fi descrise prin intermediul procedurilor inserare_la_inceput1, inserare_la_inceput2, elimina_prima1, elimina_prima2. Operaţiile elementare pentru gestiunea informaţiei memorate într-o stivă sunt: - push(S,d,test) – inserarea informaţiei d în stiva S; - pop(d,S,test) – preluarea cu eliminare a informaţiei memorate în prima celulă a stivei S; - top(d,S,test) – preluarea fără eliminare a informaţiei deţinute de prima componentă a stivei S. Parametrul test returnează true dacă operaţia este posibilă, altfel false. Coada Se numeşte coadă o listă organizată astfel încât operaţia de inserare este permisă la ultima componentă, iar operaţia de eliminare este permisă numai la prima componentă. Acest mod de organizare corespunde unei gestiuni FIFO (First In First Out) a informaţiei stocate. Modelul corespunde unei cozi de aşteptare la un magazin. O nouă persoană se aşază la coadă după ultimul cumpărător, iar persoana care îşi achită nota de plată (primul cumpărător) părăseşte coada. Implementarea unei liste coadă poate fi efectuată atât printr-o structură statică (masiv unidimensional), cât şi printr-o structură dinamică de tip listă. În scopul eficientizării operaţiilor de inserare/extragere, în cazul implementării cozilor prin structuri dinamice lineare, este necesară utilizarea a două informaţii: adresa

26

Structura de listă în limbajul Pascal

primei componente şi adresa ultimei componente. Aceste informaţii pot fi menţinute explicit prin utilizarea a doi pointeri sau prin utilizarea unui pointer şi a unei structuri de listă circulară. 1. Operaţiile de inserare şi eliminare pentru listă coadă reprezentată static sunt descrise de procedurile inserare_la_sfarsit1 şi eliminare_la_inceput prezentate în cadrul secţiunii §2.3. 2. Reprezentarea unei liste coadă printr-o structură dinamică circulară utilizează o singură variabilă adresă, ultim, pentru referirea ultimei componente a listei. Operaţiile de inserare şi eliminare sunt descrise de procedurile inserare_coada şi eliminare_coada. procedure inserare_coada(var ultim:lista; d:tip_informatie; var test:boolean); var p:lista; begin test:=true; if MaxAvail<sizeof(nod) then test:=false else begin new(p); atribuie(p^.inf,d); if ultim=nil then begin ultim:=p; ultim^.leg:=ultim; end else begin p^.leg:=ultim^.leg; ultim^.leg:=p; ultim:=p; end; end; end; procedure elimina_coada(var ultim:lista; var d:tip_informatie; var test:boolean); var prim:lista; begin test:=true; if ultim=nil then test:=false else begin prim:=ultim^.leg; atribuie(d,prim^.inf); if ultim^.leg=ultim then begin dispose(ultim); ultim:=nil; end else begin ultim^.leg:=prim^.leg; dispose(prim); end; end; end;

27

GRAFURI. IMPLEMENTĂRI ÎN LIMBAJUL PASCAL

Grafurile sunt structuri de date cu aplicaţii în multe domenii ale prelucrării automate a datelor, algoritmii pentru reprezentarea şi prelucrarea grafurilor fiind consideraţi fundamentali în acest domeniu. În cadrul secţiunii 3.1 sunt prezentate principalele caracteristici ale grafurilor, precum şi modalităţile uzuale de reprezentare a structurii de graf. În continuare sunt descrise tehnicile de parcurgere a grafurilor în lăţime şi în adâncime. Verificarea conexităţii şi calculul drumurilor în grafuri sunt tratate în secţiunea 3.3

3.1 Definiţii, caracteristici şi reprezentări ale grafurilor Definiţia 3.1.1. Un graf (sau un graf neorientat) este o structură G=(V,E), unde V este o mulţime nevidă, iar E este o submulţime (posibil vidă) a mulţimii perechilor neordonate cu componente distincte din V. Obiectele mulţimii V se numesc vârfuri, iar obiectele mulţimii E se numesc muchii. Dacă e ∈ E, e = (u, v) = uv , se spune că muchia e are ca extremităţi u,v sau că muchia e este determinată de vârfurile u şi v. Dacă e=uv ∈ E se spune că vârfurile u, v sunt incidente cu muchia e. uv ∈ E.

Definiţia 3.1.2. Fie G = (V,E) graf. Vârfurile u, v sunt adiacente în G dacă Definiţia 3.1.3. Graful G = (V,E) este finit, dacă V este o mulţime finită.

În cadrul acestui capitol vor fi considerate în exclusivitate grafurile finite, chiar dacă acest lucru nu va fi precizat în mod explicit. Definiţia 3.1.4. Fie Gi =(Vi,Ei), i=1,2 grafuri. G2 este un subgraf al grafului G1 dacă V2 ⊆ V1 şi E 2 ⊆ E 1 . Dacă G2 este un subgraf al lui G1, G2 este un graf parţial al lui G1 dacă V2=V1.

28

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Definiţia 3.1.5. Un digraf este o structură D = (V,E), unde V este o mulţime nevidă de obiecte numite convenţional vârfuri, iar E este o mulţime (posibil vidă) de perechi ordonate cu componente elemente distincte din V. Convenţional, elementele mulţimii E sunt numite arce sau muchii ordonate. Terminologia utilizată relativ la digrafuri este similară celei corespunzătoare grafurilor. Definiţia 3.1.6. Se numeşte graf ponderat o structură (V,E,W), unde G =

= (V,E) este graf, W funcţie, W : E → (0, ∞ ) . Funcţia W este numită pondere şi ea asociază fiecărei muchii a grafului un cost/câştig al parcurgerii ei. Definiţia 3.1.7. Fie G=(V,E) un graf, u,v∈V. Secvenţa de vârfuri Γ: u0, u1,..,un este un u-v drum dacă u0=u, un=v, uiui+1∈E pentru toţi i, 0 ≤ i ≤ n . Moduri de reprezentare a grafurilor Cea mai simplă reprezentare a unui graf este cea intuitivă, grafică; fiecare vârf este figurat printr-un punct, iar muchiile sunt reprezentate prin segmentele de dreaptă, orientate (în cazul digrafurilor) sau nu şi etichetate (în cazul grafurilor ponderate) sau nu, având ca extremităţi punctele corespunzătoare vârfurilor care le determină. Exemple: 3.1. Fie G = (V, E) graf, cu V = {1, 2, 3, 4, 5}, E = {(1,2),(1,3),(2,5),(3,5)}. O posibilă reprezentare grafică este:

1 2

● 4

3 5

29

Grafuri. Implementări în limbajul Pascal

3.2. Fie D = (V, E) digraf, cu V = {1, 2, 3, 4, 5}, E = {(1,2),(1,3), (2,5),(3,5),(1,5)}. Digraful poate fi reprezentat grafic astfel:

1 ● 4

2

3 5 3.3. Fie G = (V, E, W) graf ponderat, cu V = {1, 2, 3, 4}, E = {(1,2),(1,3),(2,4),(3,4)}, W((1,2))=5, W((1,3))=1, W((2,4))=2, W((1,4))=7. O posibilă reprezentare grafică este:

1 5

1 3

2 7 2 4

Deşi acest mod de reprezentare este foarte comod şi sugestiv, în special în cazul grafurilor cu număr mic de vârfuri, pentru prelucrări cu ajutorul calculatorului sunt necesare reprezentări prin intermediul structurilor de date. O modalitate de reprezentare este cea prin matrice de adiacenţă. Dacă G=(V,E) este graf sau digraf cu V = n , atunci matricea de adiacenţă A ∈ Mnxn({0,1}) are componentele: 1, dacă vi , v j ∈ E a ij =  , 0, altfel

(

)

30

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

unde vi, vj reprezintă cel de-al i-lea, respectiv cel de-al j-lea nod din V. Se observă că, în cazul unui graf neorientat, matricea de adiacenţă este simetrică ∀i, j = 1, n , a ij = a ji (perechile de vârfuri ce caracterizează muchiile sunt neordonate, deci dacă uv ∈ E, atunci şi vu ∈ E), în timp ce, în cazul unui digraf, este posibil ca v i , v j ∈ E, v i , v j ∉ E , deci aij ≠ aji.

(

)

(

)

Exemplu: 3.4. Graful din exemplul 3.1 şi digraful din exemplul 3.2 sunt reprezentate prin matricele de adiacenţă:

0 1 1 0 0  0 1 1    1 0 0 0 1  0 0 0   A = 1 0 0 0 1 pentru 3.1, A =  0 0 0    0 0 0 0 0 0 0 0  0 1 1 0 0 0 0 0   

0 0 0 0 0

1  1 1 pentru 3.2  0 0 

În cazul grafurilor ponderate, reprezentarea matriceală este asemănătoare celei prezentate anterior. Matricea ponderilor unui graf ponderat G = (V, E, W), V = n , W ∈ Mnxn((0, ∞ )) are componentele:

W (( v i , v j ) ), dacă (v i , v j ) ∈ E , w i, j =  α, altfel

unde vi, vj reprezintă cel de-al i-lea, respectiv cel de-al j-lea nod din V, α =0 dacă ponderea are semnificaţia de câştig, respectiv α = ∞ în cazul în care se doreşte reprezentarea costurilor ca ponderi ale grafului. Exemplu: 3.5. Presupunând că ponderile reprezintă costuri, matricea de reprezentare a grafului din exemplul 3.3. este:

∞  5 W= 1  7 

5 1 7  ∞ ∞ 2 . ∞ ∞ ∞  2 ∞ ∞ 

Reţinând numai “informaţia utilă”, şi anume existenţa unei muchii între două vârfuri şi eventual valoarea ponderii ei, se obţine reprezentarea tabelară, mai economică din punctul de vedere al spaţiului de memorare. În cazul în care există vârfuri izolate în graf (ce nu sunt incidente cu nici o muchie), atunci este necesară păstrarea acestora într-un vector suplimentar. Mulţimea muchiilor se memorează

31

Grafuri. Implementări în limbajul Pascal

într-o matrice cu E linii şi 2 coloane dacă graful nu este ponderat, respectiv cu 3 coloane, dacă graful este ponderat. În primele două coloane se scriu perechile de vârfuri ce determină muchiile, în cazul grafurilor ponderate cea de-a treia coloană conţine valoarea ponderii muchiei respective. Exemple: 3.6. Graful din exemplul 3.1 poate fi reprezentat astfel: deoarece 4 este vârf izolat, vectorul suplimentar este VS = (4), pentru reprezentarea muchiilor fiind

1  1 utilizată matricea A =  2  3 

2  3 5  5 

3.7. Digraful din exemplul 3.2 poate fi reprezentat astfel: deoarece 4 este vârf izolat, vectorul suplimentar este VS = ( 4), arcele fiind reprezentate prin

1  1 A = 1  2 3 

2  3 5  5 5 

O altă reprezentare este prin intermediul listelor. Reprezentarea permite utilizarea economică a spaţiului de memorare şi, în anumite cazuri, implementări mai eficiente pentru anumite clase de algoritmi. Vârfurile grafului se memorează într-o listă, fiecare celulă a listei având o legătură către lista vecinilor acelui vârf (vârfurile din graf adiacente cu vârful corespunzător acelei celule şi indicat ca informaţie utilă). În situaţia în care graful nu este ponderat, el se reprezintă printr-o listă de liste, şi anume: nodurile grafului se trec într-o listă L_nod, fiecare celulă având structura informaţie legătură vecini legătură nod următor unde: • câmpul informaţie conţine identificatorul nodului; • legătură vecini reprezintă pointer către capul listei vecinilor; • legătură nod următor conţine adresa următoarei celule din lista L_nod. Un graf ponderat poate fi reprezentat în mod similar, cu diferenţa că fiecare celulă din lista vecinilor conţine şi ponderea muchiei respective (muchia care are ca extremităţi vârful referit prin identificatorul de nod din lista vecinilor şi respectiv vârful indicat de informaţia acelei celule din L_nod ce conţine adresa primului element al listei vecinilor).

32

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

3.2 Modalităţi de parcurgere a grafurilor Parcurgerea unui graf reprezintă o modalitate de vizitare a tuturor vârfurilor grafului, fiecare vârf fiind vizitat o singură dată. În acest paragraf sunt prezentate două modalităţi de parcurgere a grafurilor neorientate. Ambele metode de parcurgere presupun selectarea unui vârf iniţial v0. Prin aplicarea acestor metode sunt identificate numai vârfurile grafului cu proprietatea că există cel puţin un drum de la vârful iniţial către acel vârf. Grafurile cu proprietatea că oricare două vârfuri sunt conectate printr-un drum se numesc conexe şi sunt prezentate în § 3.3. Dacă graful este conex, atunci prin aplicarea metodelor de parcurgere vor fi identificate toate vârfurile grafului.

3.2.1 Metoda de parcurgere BF (Breadth First) Ideea traversării BF este de parcurgere în lăţime a grafului, în sensul că vârfurile grafului sunt prelucrate în ordinea crescătoare a “distanţelor” la vârful iniţial. Prin distanţă se înţelege numărul de muchii din drumul identificat la acel moment de la vârful iniţial către acel vârf. La momentul iniţial, vârful curent este v0. Deoarece vârful curent la fiecare moment trebuie să fie unul aflat la distanţă minimă de v0, se poate proceda în modul următor: iniţial lui v0 i se asociază valoarea 0 şi fiecărui vârf diferit de v0 i se asociază valoarea –1. Dacă valoarea asociată vârfului curent este m, atunci fiecăruia dintre vecinii acestuia de valoare –1 i se asociază valoarea m+1. Se observă că dacă după ce toate vârfurile de valoare m au fost considerate şi nici unui vârf nu i-a fost recalculată valoarea, atunci toate vârfurile conectate cu v0 au fost găsite, deci calculul se încheie. Exemple: 3.8 Fie graful:

1 2

3 4

5

6 7

şi v0=1.

33

Grafuri. Implementări în limbajul Pascal

Valorile calculate prin aplicarea metodei prezentate sunt: vârf

1

2

3

4

5

6

7

0 -1 -1 -1 -1 0 1 1 -1 1 0 1 1 2 1 0 1 1 2 1 Ordinea de vizitare a vârfurilor: 1,2,3,5,7,4,6.

-1 -1 2 2

-1 1 1 1

m 0 1 2

3.9. Fie graful:

1

8

2

3 4

6

9

10

7 5 şi v0=1. Se observă că vârfurile 8, 9 şi 10 nu sunt conectate cu vârful iniţial. Valorile rezultate prin aplicarea metodei sunt: vârf

1

2

3

4

5

6

7

8

9

10

-1 1 1 1

-1 -1 -1 -1

-1 -1 -1 -1

-1 -1 -1 -1

m 0 1 2

0 -1 -1 -1 -1 -1 0 1 1 -1 1 -1 0 1 1 2 1 2 0 1 1 2 1 2 Ordinea de vizitare a vârfurilor este 1,2,3,5,7,4,6.

Se observă că valorile lui m calculate în final reprezintă numărul de muchii corespunzător celui mai scurt drum care conectează vârful iniţial cu vârful respectiv, pentru vârfurile neconectate cu v0 valoarea lui m rezultată la terminarea calculului este –1. O implementare diferită, dar urmând aceeaşi idee, rezultă prin utilizarea următoarelor structuri de date: • A matricea de adiacenţă a grafului; • o structură de tip coadă, C, în care sunt introduse vârfurile ce urmează a fi vizitate şi procesate (în sensul cercetării vecinilor lor);

34

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

• Un vector c cu n componente, unde: 1, dacă i a fost introdus în coadă ci =  altfel 0, şi n este numărul vârfurilor grafului. Componentele vectorului c vor fi iniţializate cu valoarea 0. Descrierea metodei este: • se iniţializează coada C cu vârful v0; • cât timp coada este nevidă, se extrage un vârf i din coadă, se vizitează, apoi se introduc în coadă numai vecinii acestuia care nu au fost deja introduşi (adică toate vârfurile k având proprietatea că c[k]=0 şi a[i,k]=1). Vârfurile i ce au fost introduse în coadă sunt marcate: ∀i introdus în coadă, c[i]=1. În continuare este prezentat un program Pascal pentru parcurgerea în lăţime a unui graf. Vârfurile grafului sunt numerotate de la 1 până la n, parcurgerea începând cu vârful numerotat cu 1. Graful este reprezentat prin matricea de adiacenţă. Structura de coadă este implementată ca o listă simplu înlănţuită, procedurile push şi pop realizând introducerea, respectiv extragerea unui element din coadă. uses crt; type ptcoada=^coada; coada=record inf:byte; leg:ptcoada; end; var prim,ultim:ptcoada; c:array[1..20] of byte; a:array[1..20,1..20] of byte; i,j,k,n:byte; procedure push(var p,u:ptcoada;i:byte); var a:ptcoada; begin new(a); a^.inf:=i; a^.leg:=nil; if p=nil then begin p:=a; u:=a; end else begin u^.leg:=a;u:=a; end; end; procedure pop(var p,u:ptcoada;var i:byte); var a:ptcoada; begin if p<>nil then begin i:=p^.inf; a:=p; p:=p^.leg; dispose(a); if p=nil then u:=nil; end;

35

Grafuri. Implementări în limbajul Pascal end; begin {program principal} clrscr; write('Numarul de varfuri:'); readln(n); writeln('Matricea de adiacenta'); for i:=1 to n do for j:=1 to n do begin write('a[',i,',',j,']='); readln(a[i,j]); end; for i:=2 to n do c[i]:=0; readln; clrscr; writeln('Incepem parcurgerea de la nodul 1'); c[1]:=1; prim:=nil;ultim:=nil; push(prim,ultim,1); while prim<>nil do begin pop(prim,ultim,i); write(i,' '); for k:=1 to n do if (a[i,k]=1)and(c[k]=0) then begin c[k]:=1; push(prim,ultim,k); end; end; readln; end.

3.2.2 Metoda de parcurgere DF (Depth First) Ideea metodei DF revine la parcurgerea în adâncime a grafurilor, în sensul că, la fiecare moment, dacă M este mulţimea vârfurilor vizitate de procedură, pentru vizitarea vecinilor este considerat unul din vârfurile din M cu proprietatea că lungimea drumului calculat până la vârful iniţial v0 este maximă. Implementarea metodei poate fi realizată în mai multe moduri, pentru menţinerea mulţimii vârfurilor grafului disponibilizate până la momentul curent fiind utilizată o structură de date de tip stivă, S. La momentul iniţial se introduce în stivă v0. La fiecare pas, se preia cu ştergere ca vârf curent vârful stivei S şi se introduc în stivă vecinii încă nevizitaţi ai vârfului curent. Un vârf se marchează ca vizitat în momentul introducerii lui în S. Calculul continuă până când este efectuat un acces de preluare din stivă şi se constată că S este vidă. Pentru gestiunea vârfurilor vizitate, se utilizează un vector c cu n componente, unde n reprezintă numărul vârfurilor grafului şi, la fiecare moment, componentele sunt:

36

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

1, dacă i a fost vizitat ci =  0 , altfel Componentele vectorului c vor fi iniţializate cu valoarea 0. Exemple: 3.10. Pentru graful:

1 2

3 4

6

5

7

şi v0 = 1, ordinea în care sunt vizitate vârfurile este: 1, 2, 3, 4, 6, 7, 5. 3.11. Pentru graful

1 2

8 3

4

6

9

10

7 5 şi v0 = 1, ordinea în care sunt vizitate vârfurile este: 1, 2, 3, 4, 6, 7, 5. Vârfurile 8, 9 şi 10 nu sunt vizitate de procedură, pentru că nu sunt conectate de vârful iniţial selectat. O variantă de implementare a metodei DF rezultă prin gestionarea stivei S după cum urmează. Iniţial vârful v0 este unicul component al lui S . La fiecare etapă se preia, fără ştergere, ca vârf curent vârful stivei. Se introduce în stivă unul dintre vecinii vârfului curent încă nevizitat. Vizitarea unui vârf revine la introducerea lui în S. Dacă vârful curent nu are vecini încă nevizitaţi, atunci este eliminat din stivă şi

37

Grafuri. Implementări în limbajul Pascal

este efectuat un nou acces de preluare a noului vârf al stivei ca vârf curent. Calculul se încheie în momentul în care este efectuat un acces de preluare a vârfului stivei ca vârf curent şi se constată că S este vidă. Evident, nici în cazul acestei variante nu vor fi vizitate vârfurile care nu sunt conectate cu vârful ales iniţial.

3.3 Drumuri în grafuri. Conexitate 3.3.1 Drumuri; definiţii Una dintre cele mai importante proprietăţi ale grafurilor o constituie posibilitatea de accesare, prin intermediul unei secvenţe de muchii (arce), dintr-un vârf dat a oricărui alt vârf al grafului, proprietate cunoscută sub numele de conexitate sau conexiune. Aşa după cum a rezultat în §3.2., dacă G=(V,E) este un graf conex, atunci pentru orice vârf iniţial v0 considerat, metodele BF şi DF permit vizitarea tuturor vârfurilor din V. Definiţia 3.3.1. Fie G=(V,E) un graf, u,v∈V. Secvenţa de vârfuri Γ: u0, u1,..,un este un u-v drum dacă u0=u, un=v, uiui+1∈E pentru toţi i, 0 ≤ i ≤ n . Lungimea drumului, notată l(Γ),este egală cu n. Convenţional, se numeşte trivial, un drum Γ cu l(Γ)=0. Definiţia 3.3.2. Fie Γ: u0, u1,..,un un drum în graful G=(V,E). Γ este un drum închis dacă u0=un; în caz contrar, Γ este deschis. Drumul Γ este elementar dacă oricare două vârfuri din Γ sunt distincte, cu excepţia, eventual, a extremităţilor. Drumul Γ este proces dacă, pentru orice 0 ≤ i ≠ j ≤ n − 1 , uiui+1 ≠ ujuj+1. Evident, orice drum elementar este un proces. Definiţia 3.3.3. Fie Γ: u0, u1,..,un un drum în graful G = (V,E). Γ’: v0, v1,..,vm este un subdrum al lui Γ dacă Γ’ este un drum şi pentru orice j, 0 ≤ j ≤ m , există i, 0 ≤ i ≤ n , astfel încât ui = vj. Evident, orice drum cu lungime cel puţin 1 conţine cel puţin un drum elementar cu aceleaşi extremităţi. Într-adevăr, dacă Γ: u0, u1,..,un nu este elementar, atunci există 0 ≤ i < j ≤ n şi i ≠ 0 sau j ≠ n, astfel încât ui = uj.

Atunci drumul

38

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

u j u j+1 ...u n , dacă i = 0  Γ : u 0 u 1 ...u i , dacă j = 0 u u ...u u ...u , dacă i ≠ 0, j ≠ n  0 1 i j+1 n '

este, de asemenea, un u0-un drum. Aplicând în continuare eliminarea duplicatelor vârfurilor în modul descris, rezultă în final un u0-un drum elementar.

3.3.2 Matricea existenţei drumurilor; algoritmul Roy-Warshall Fie G=(V,E) un graf, V = n . Dacă A este matricea de adiacenţă asociată grafului, atunci, pentru orice p≥1, a ij( p ) este numărul vi-vj drumurilor distincte de

( )

lungime p din graful G, unde A p = a ij( p ) . Definiţia 3.3.4. Fie Mn({0,1)} mulţimea matricelor de dimensiuni nxn, componentele fiind elemente din mulţimea {0,1}. Pe Mn({0,1)}se definesc operaţiile

binare, notate ⊕ şi ⊗ , astfel: pentru orice A=(aij), B=(bij) din Mn({0,1)}, A ⊕ B=(cij), A ⊗ B=(dij), unde

1 ≤ i, j ≤ n , cij=max{aij, bij} dij=max{min{aik, bkj}, 1 ≤ k ≤ n }. Dacă A=(aij) ∈ Mn({0,1)}, se notează

{A = (a ); k ≥ 1} secvenţa de k

(k) ij

matrice definită prin:

A

(1)

k

= A, A = A ⊗ A

( k −1)

, ∀k ≥ 2 .

Dacă A este matricea de adiacenţă a unui graf G=(V,E), atunci pentru fiecare (k )

1, dacă există drum de la i la j de lungime k 0, altfel

k, 1 ≤ k ≤ n − 1 , a ij = 

M=A

(1)

⊕A

( 2)

⊕Κ ⊕ A

( n −1)

se numeşte matricea existenţei drumurilor

în graful G. Semnificaţia componentelor matricei M este:

0, dacă nu există v i − v j drum în G ∀1 ≤ i, j ≤ n , m ij =  1, altfel

39

Grafuri. Implementări în limbajul Pascal

Exemplu: 3.12. Pentru graful:

2 1 3

4 0  1 A= 1  1 

1 1 1 1 0   0 0 0 2  0 1 ,A = 0 0 1 1 1   1 1 0 1 0  

1 1 1 1   1 1 3 1 0 ,A = 1 1 1 1    1 1 1 1

1 1 1 1   1 1 1 1 ,M =  1 1 1 1     1 1 1 1

1 1  1 1 1 1  1 1

Calculul matricei existenţei drumurilor permite verificarea faptului că un graf dat este conex: graful este conex dacă şi numai dacă toate componentele matricei M sunt egale cu 1. Algoritmul Roy-Warshall calculează matricea existenţei drumurilor într-un graf G cu n vârfuri.

procedure Roy_Warshall (a,n,m); i,j,k:integer; do-for i=1,n,1 do-for j=1,n,1 mij=aij; do-for j=1,n,1 do-for i=1,n,1 if mij =1 then do-for k=1,n,1 if mik<mkj then mik=mkj; endif; enddo; endif; enddo; enddo; end;

40

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Datele de intrare sunt: n, numărul de noduri şi A, matricea de adiacenţă corespunzătoare grafului. Matricea M calculată de algoritm constituie ieşirea şi este matricea existenţei drumurilor în graful G.

3.3.3 Componente conexe ale unui graf Definiţia 3.3.5. Fie G=(V,E) graf netrivial. Vârfurile u,v ∈ V sunt conectate dacă există un u-v drum în G. Definiţia 3.3.6. Dacă G este un graf, atunci o componentă conexă a lui G este un subgraf conex al lui G, maximal în raport cu proprietatea de conexitate. Evident, un graf este conex dacă şi numai dacă numărul componentelor sale conexe este 1. Mulţimile vârfurilor corespunzătoare oricăror două componente conexe distincte sunt disjuncte. Mulţimile vârfurilor corespunzătoare componentelor conexe ale unui graf formează o partiţie a mulţimii vârfurilor grafului. Multe aplicaţii modelate în termeni de grafuri impun determinarea componentelor conexe corespunzătoare unui graf dat. Problema poate fi rezolvată în modul următor: se selectează un vârf al grafului, se determină componenta conexă care-l conţine; dacă există vârfuri care nu aparţin componentei conexe determinate, se alege unul dintre acele vârfuri căruia i se determină componenta conexă care-l conţine; în continuare, se repetă procedeul până când au fost găsite toate componentele conexe ale grafului. Pentru G=(V,E), V = n , n ≥ 1 şi v0 ∈ V, paşii algoritmului pentru

determinarea componentei conexe care conţine un vârf v0 dat sunt: Pasul 1: V0={v0}; E0= Φ ; i=0; Pasul 2: repetă Pas 3 până când Vi=Vi-1 şi Ei=Ei-1 Pasul 3: i=i+1;

Vi = Vi −1 ∪ {v / v ∈ V, ∃u ∈ Vi −1 , uv ∈ E};

E i = E i −1 ∪ {e / e ∈ E, ∃u ∈ Vi −1 , u incident cu e};

Ieşirea este G1=(Vi,Ei), componenta conexă din care face parte v0. Exemplu: 3.13. Pentru graful 1

2

7

3

4

5

8

6

41

Grafuri. Implementări în limbajul Pascal

Aplicarea algoritmului descris, pentru v0=1, determină următoarea evoluţie: i i=0 i=1 i=2 i=3 i=4

Vi {1} {1,2,4} {1,2,4,7,5} {1,2,4,7,5,8} {1,2,4,7,5,8}

Ei Ø {(1,2),(1,4)} {(1,2),(1,4),(2,7),(4,5),(4,7)} {(1,2),(1,4),(2,7),(4,5),(4,7),(5,8),(7,8)} {(1,2),(1,4),(2,7),(4,5),(4,7),(5,8),(7,8)}

3.3.4 Drumuri de cost minim Definiţia 3.3.7. Fie G=(V, E, w) un graf ponderat. Costul drumului Γ: u1, u2,..,un, notat L(Γ), este definit prin: n −1

L(Γ ) = ∑ w (u i , u i +1 ) . i =1

Pentru orice u şi v vârfuri conectate în G, u ≠ v, w-distanţa între u şi v, notată D(u,v), este definită prin: D(u, v ) = min{L(Γ ), Γ ∈ D uv }, unde Duv desemnează mulţimea tuturor u-v drumurilor elementare din G. Dacă Γ ∈ D uv este astfel încât D(u,v)=L(Γ), drumul Γ se numeşte de cost minim. Cu toate că este utilizat termenul de w-distanţă, în general D nu este o distanţă în sensul matematic al cuvântului. În particular, dacă funcţia pondere asociază valoarea 1 fiecărei muchii a grafului, atunci, pentru fiecare pereche de vârfuri distincte ale grafului, costul D(u,v) este lungimea celui mai scurt drum între cele două vârfuri. În acest caz, D este o distanţă pe mulţimea vârfurilor. Dat fiind interesul pentru determinarea w-distanţelor şi a drumurilor de cost minim în cadrul aplicaţiilor modelate pe grafuri, în continuare vor fi prezentaţi algoritmi pentru rezolvarea acestor probleme. Algoritmul Dijkstra

Algoritmul a fost propus de E. W. Dijkstra pentru determinarea w-distanţelor D(u0,v) şi a câte unui u0-v drum de cost minim pentru fiecare vârf v≠u0 într-un graf ponderat, unde u0 este prestabilit. Fie (V,E,w) graf conex ponderat, u0∈V, S⊂V, u0∈S. Se notează S = V \ S şi

(

)

{

}

D u 0 , S = min D(u 0 , x ); x ∈ S . Fie v∈ S astfel încât D(u0,v)=D(u0, S ), Γ : u0, u1,…,upv este un u0-v drum de cost minim. Evident, ∀0≤i≤p ui∈S şi Γ ’: u0, u1,…,up, un u0- up drum de cost minim. De asemenea,

(

)

{

}

D u 0 , S = min D(u 0 , u ) + w (uv); u ∈ S, v ∈ S, uv ∈ E .

42

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Dacă

x∈S,

y∈ S

astfel

încât

(

)

D u 0 , S = D(u 0 , x ) + w ( xy) , rezultă

D(u 0 , y ) = D(u 0 , x ) + w ( xy) . Pentru determinarea unui drum u0-v, cel mai ieftin, algoritmul consideră o etichetare dinamică a vârfurilor grafului. Eticheta vârfului v este (L(v),u), unde L(v) este lungimea celui mai ieftin drum u0-v determinat până la momentul respectiv şi u este predecesorul lui v pe un astfel de drum. Pentru (V,E,w) graf conex ponderat, V = n şi u0∈V, calculul implicat de algoritmul Dijkstra poate fi descris astfel: Pasul 1: i=0; S0={u0}; L(u0)=0, L(v)= ∞ pentru toţi v ∈ V, v≠u0. Dacă n=1 atunci stop Pasul 2: Pentru toţi v∈ Si , dacă L(v)>L(ui)+w(uiv), atunci L(v)=L(ui)+w(uiv) şi etichetează v cu (L(v),ui). Pasul 3: Se determină d=min{L(v), v∈ S i } şi se alege ui+1∈ S i astfel încât L(ui+1)=d. Pasul 4: Si+1=Si ∪ {ui+1} Pasul 5: i=i+1. Dacă i=n-1, atunci stop. Altfel, reia Pasul 2.

Evident, dacă (V,E,w) este graf ponderat neconex, atunci, pentru u0∈V, algoritmul lui Dijkstra permite determinarea w-distanţelor D(u0,v) şi a câte unui u0-v drum de cost minim pentru toate vârfurile v din componenta conexă căreia îi aparţine u0. Exemplu:

3.14. Fie graful ponderat

1 5

1 9

2

3 2

16 5 5 4

43

Grafuri. Implementări în limbajul Pascal

Considerând u0=1, etapele în aplicarea algoritmului Dijkstra sunt: P1: i=0; S0={1}; L(1)=0, L(i)= ∞ pentru toţi i = 2,5 . P2: S 0 ={2,3,4,5}, u0=1 L(2)= ∞ >L(1)+5=5 ⇒ L(2)=5, etichetează 2 cu 1 L(3)= ∞ >L(1)+1=1 ⇒ L(3)=1, etichetează 3 cu 1 L(4)= ∞ >L(1)+9=9 ⇒ L(4)=9, etichetează 4 cu 1 L(5)= ∞ , w(1,5)= ∞ , deci L(5) nu se modifică P3: selectează u1=3, L(3)=1, cea mai mică dintre w-distanţele calculate la P2 P4: S1={1,3} P5: i=i+1=1 ≠ 4, reia P2 P2: S1 ={2,4,5}, u1=3 Nu se modifică nici o etichetă şi nici o w-distanţă (w(3,i)= ∞ , pentru toţi i din S1 ) P3: selectează u2=2, L(2)=5, cea mai mică dintre w-distanţele calculate la P2 P4: S2={1,3,2} P5: i=i+1=2 ≠ 4, reia P2 P2: S 2 ={4,5}, u2=2 L(4)= 9>L(2)+2=7 ⇒ L(4)=7, etichetează 4 cu 2 L(5)= ∞ >L(2)+16=21, etichetează 5 cu 2 P3: selectează u3=4, L(4)=7, cea mai mică dintre w-distanţele calculate la P2 P4: S3={1,3,2,4} P5: i=i+1=3 ≠ 4, reia P2 P2: S3 ={5}, u3=4 L(5)= 21>L(4)+5=12, etichetează 5 cu 4 P3: selectează u4=5, L(5)=12, cea mai mică dintre w-distanţele calculate la P2 P4: S3={1,3,2,4,5} P5: i=i+1=4, stop. Algoritmul calculează următoarele rezultate: Vârful v până la care se calculează w-distanţa D(1,v), eticheta lui v

1

2

3

4

5

0, 1

5, 1

1, 1

7, 2

12, 4

Drumurile de cost minim de la vârful 1 la fiecare dintre vârfurile grafului se stabilesc pe baza sistemului de etichete astfel: drumul de la 1 la un vârf v este dat de: v1, eticheta lui v, v2 eticheta lui v1 ş.a.m.d., până se ajunge la eticheta 1.

44

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Astfel, v0-drumurile de cost minim sunt: până la 2: 2,1; până la 3: 3,1; până la 4: 4,2,1; până la 5: 5,4,2,1. Programul Pascal pentru algoritmul Dijkstra, graful ponderat fiind reprezentat sub formă tabelară, este prezentat în continuare. Variabilele folosite în program au următoarele semnificaţii: - v: numărul de vârfuri ale grafului; - m: numărul de muchii din graf; - s: mulţimea Si la momentul curent; - t: mulţimea Si la momentul curent; - l: vectorul componentelor L din etichetele ataşate vârfurilor; - dr: vectorul componentelor de tip vârf din etichetele ataşate vârfurilor. uses crt; var a:array[1..200,1..3] of integer; s,dr,t,l:array[1..20] of integer; i,j,k,p,v1,u,c,d:integer; m,v:integer; begin clrscr; readln(v); readln(m); for i:=1 to m do for j:=1 to 3 do begin write('a[',i,',',j,']='); readln(a[i,j]); end; i:=1;s[1]:=1;v1:=1; for j:=2 to v do begin t[j-1]:=j; l[j]:=maxint; end; l[1]:=0; for k:=1 to v-1 do begin for j:=1 to v-i do begin u:=1; while((t[j]<>a[u,1])or(v1<>a[u,2]))and ((t[j]<>a[u,2])or(v1<>a[u,1]))and(u<=m) do inc(u); if(u<=m) and (l[t[j]]>l[v1]+a[u,3]) then begin l[t[j]]:=l[v1]+a[u,3]; dr[t[j]]:=v1; end; end; d:=l[t[1]];v1:=t[1]; for j:=2 to v-i do if l[t[j]]
45

Grafuri. Implementări în limbajul Pascal v1:=t[j]; end; inc(i); s[i]:=v1; u:=1; while(u<=v-i+1)and(t[u]<>v1) do inc(u); for j:=u to v-i do t[j]:=t[j+1]; end; clrscr; for i:=1 to v do writeln(i,'--->',l[i]); writeln('Drumurile minime de la fiecare varf la 1:'); for j:=2 to v do begin k:=j;write(k,' '); while k<>1 do begin write(dr[k],' '); k:=dr[k]; end; writeln; end; readln; end.

În anumite cazuri se doreşte determinarea numai a w-distanţelor D(v0,v), pentru toţi v∈V. În acest caz, algoritmul Roy-Floyd permite o rezolvare a acestei probleme mai simplu de implementat decât algoritmul Dijkstra. Algoritmul Roy-Floyd

Pentru (V,E,w) graf ponderat, V = n şi W matricea ponderilor, sistemul de w-distanţe D(v0,v), v∈V, poate fi calculat pe baza următoarei proceduri (similară algoritmului Roy-Warshall): procedure Roy_Floyd (w,n,d); i,j,k:integer; do-for i=1,n,1 do-for j=1,n,1 dij=wij; do-for j=1,n,1 do-for i=1,n,1 if dij ≠ ∞ then do-for k=1,n,1 if dik>dij+ djk then dik=dij+ djk; endif; enddo; endif; enddo; enddo;

46

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

end; Matricea D calculată de algoritm este a w-distanţelor D(u,v) în graful ponderat conex (V,E,w); pentru orice 1 ≤ i, j ≤ n

D( v i , v j ), v i , v j sunt conectate d ij =  ∞, altfel Într-adevăr, procedura calculează dinamic w-distanţa între oricare două vârfuri i şi k, astfel: dacă există un i-k drum ce trece prin j ( 1 ≤ j ≤ n ), cu costul corespunzător (dij+ djk) inferior costului curent (dik), atunci noul drum de la i la k via j este de cost mai mic decât cel al drumului vechi, deci w-distanţa între i şi k trebuie reactualizată la dij+ djk. Algoritmul Yen

Pentru calculul tuturor w-distanţelor într-un graf ponderat, algoritmul propus de Yen rezolvă problema într-o manieră mai eficientă decât algoritmul Roy-Floyd, din punctul de vedere al volumului operaţiilor. Fie (V,E,w) un graf ponderat, W matricea ponderilor. Pentru determinarea w-distanţelor de la vârful vk fixat la celelalte vârfuri ale grafului, algoritmul Yen iniţiază următoarele operaţii: Pasul 1: D = W Pasul 2: i = 1; λ(k) = 0, b(k) = 0; λ(j)=0, pentru toţi 1 ≤ j ≤ n , j ≠ k Pasul 3: Calculează min{dkj; 1 ≤ j ≤ n , λ(j)=1}; determină j0 astfel încât

λ(j0) = 1 şi d kj0 = min{dkj; 1 ≤ j ≤ n , λ(j) = 1} B(j0) = d kj0 , λ(j0) = 0

d[k,j] = min{d[k,j],d[k,j0]+d[j0,j]}, pentru toţi j, 1 ≤ j ≤ n i=i+1 Pasul 4: Dacă i
47

STRUCTURI ARBORESCENTE

În clasa grafurilor conexe, structurile cele mai simple, dar care apar cel mai frecvent în aplicaţii, sunt cele arborescente (arbori). În acest capitol sunt prezentate principalele caracteristici ale arborilor, algoritmi pentru calculul arborelui parţial de cost minim, arbori direcţionaţi, arbori cu rădăcină şi arbori binari. Pe lângă operaţiile primitive asupra arborilor – căutarea unei informaţii, inserarea unui nod, extragerea unui nod şi metode de parcurgere, sunt prezentate două clase importante de arbori binari, şi anume arbori de sortare şi arbori de structură.

4.1 Grafuri de tip arbore 4.1.1 Definiţii şi caracterizări ale grafurilor de tip arbore Definiţia 4.1.1. Graful G este arbore dacă G este aciclic şi conex. Definiţia 4.1.2. Fie G=(V,E) graf arbore. Subgraful H=(V1,E1) al lui G este un subarbore al lui G dacă H este graf arbore. Exemple: 4.1. Graful

1 4

3 2 5

este arbore, deoarece pentru orice pereche de vârfuri i,j, 1 ≤ i,j ≤ 6, i≠j, există un i-j drum şi graful nu conţine cicluri.

48

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

4.2. Graful

1 4

3 2 5 6

nu este arbore deoarece este conex, dar drumul Γ :1,4,3,2,1 este un ciclu. 4.3. Graful

1 4

3

7

2 5 6

8

nu este arbore deoarece are două componente conexe {1,2,3,4,5,6}, {7,8}. Proprietatea unui graf de a fi arbore poate fi verificată prin algoritmi care testează calităţile de conexitate şi aciclicitate. Verificarea proprietăţii unui graf de a fi arbore poate fi realizată şi pe baza următoarelor proprietăţi: Proprietatea 1.: Un graf G=(V,E), cu V = n , E = m este de tip arbore dacă şi numai dacă G este aciclic şi n=m+1. Cu alte cuvinte, problema revine la verificarea aciclicităţii grafului şi a relaţiei existente între numărul vârfurilor şi numărul muchiilor grafului. Proprietatea 2: Un graf G=(V,E), cu V = n , E = m este de tip arbore dacă şi numai dacă G este conex şi n=m+1. Fie G=(V,E) un graf. Următoarele afirmaţii sunt echivalente: 1. G este graf arbore; 2. G este graf conex minimal (oriare ar fi e∈E, prin eliminarea muchiei e graful rezultat nu este conex);

49

Structuri arborescente

3. G este graf aciclic maximal (prin adăugarea unei noi muchii în graf rezultă cel puţin un ciclu). Definiţia 4.1.3. Un graf orientat D=(V,E), cu proprietatea că pentru ∀u, v ∈ E, u, v ∈ E , atunci vu ∉ E se numeşte graf asimetric. Digraful D este simetric dacă u , v ∈ E, uv∈E, dacă şi numai dacă vu∈E. Definiţia 4.1.4. Fie D=(V,E) digraf netrivial. Graful G=(V,E’), unde E’={uv/ uv∈E sau vu∈E} se numeşte suport al digrafului D. Definiţia 4.1.5. Un arbore direcţionat este un graf orientat asimetric cu proprietatea că graful suport corespunzător lui este graf arbore. Arborele direcţionat T=(V,E) este cu rădăcină dacă există r∈V astfel încât, pentru orice u∈V, u ≠ r, există r-u drum în T. Vârful r se numeşte rădăcina arborelui direcţionat T. Definiţia 4.1.6. Dacă T=(V,E) este arbore direcţionat, atunci T1=(V1,E1) este subarbore al lui T dacă V1⊆V, E1⊆E şi T1 este arbore direcţionat. Deoarece graful suport al unui arbore direcţionat este aciclic, rezultă că pentru orice u∈V, u ≠ r, r-u drumul în T este unic. De asemenea, un arbore direcţionat are cel mult o rădăcină. În consecinţă, pentru orice u∈V, u ≠ r, distanţa de la rădăcină la vârful u este egală cu numărul de muchii ale r-u drumului în T.

Exemple: 4.4. Arborele direcţionat

u w t

r

v

x y

z

este cu rădăcină (vârful r este rădăcina arborelui).

50

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

4.5. Arborele direcţionat

u

v x y

z

w

nu are rădăcină. 4.6. Arborele

y

w

x v este un subarbore cu rădăcină x al arborelui din exemplul 4.5

4.1.2 Arbori orientaţi; reprezentări şi parcurgeri Definiţia 4.1.7. Un arbore orientat este un arbore direcţionat cu rădăcină. Deoarece un arbore orientat este un caz particular de digraf, pentru reprezentarea unui arbore orientat poate fi utilizată oricare din modalităţile prezentate în §3.1. În plus, există şi posibilitatea obţinerii unor reprezentări mai eficiente pentru acest tip de graf. Una dintre modalităţi este reprezentarea FIU-FRATE, care constă în numerotarea convenţională a vârfurilor grafului şi reţinerea, pentru fiecare vârf i al arborelui, a următoarelor informaţii: - FIU(i), care reprezintă numărul ataşat primului descendent al vârfului i; - FRATE(i), care reprezintă numărul ataşat vârfului descendent al tatălui vârfului i şi care urmează imediat lui i; - INF(i), care reprezintă informaţia ataşată vârfului i (de obicei valoarea i). Pentru reprezentarea arborelui se reţin rădăcina şi numărul nodurilor. Absenţa “fiului”, respectiv a “fratelui” unui vârf, este marcată printr-o valoare diferită de numerele ataşate vârfurilor (de obicei valoarea 0).

51

Structuri arborescente

Exemplu: 4.7. Arborele orientat 1

2

5

3

6

10

11

7

12

4

8

13

9

14

15

este reprezentat astfel: N=15 (numărul nodurilor arborelui) R=1 (rădăcina), FIU=(2,5,7,9,0,10,0,0,13,0,0,0,0,0,0)

“fiul” lui 1 este 2

vârful 9 are “fiul” 13

FRATE=(0,3,4,0,6,0,8,0,0,11,12,0,14,15,0) vârful 1 nu are frate

vârful 14 are fratele 15

Utilizând structurile de date dinamice pentru arbori orientaţi, se obţine o reprezentare descrisă în continuare. Presupunând că fiecare vârf al arborelui are cel mult n descendenţi, fiecărui vârf îi este ataşată structura: identificatorul vârfului

vector de legături către descendenţii vârfului legătură către fiul 1 legătură către fiul n ………

Dacă un vârf are p
52

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

O parcurgere revine la aplicarea sistematică a unei reguli de vizitare a vârfurilor grafului. Cele mai uzuale reguli de parcurgere a arborilor orientaţi sunt prezentate în continuare.

A. Parcurgerea în A-preordine Iniţial vârful curent este rădăcina arborelui. Se vizitează vârful curent şi sunt identificaţi descendenţii lui. Se aplică aceeaşi regulă de vizitare pentru arborii care au ca rădăcini descendenţii vârfului curent, arborii fiind vizitaţi în ordinea dată de numerele ataşate vârfurilor rădăcină corespunzătoare.

Exemplu: 4.8. Pentru arborele orientat din exemplul 4.7, prin aplicarea parcurgerii în A-preordine, rezultă: 1,2,5,6,10,11,12,3,7,8,4,9,13,14,15. Pentru un arbore reprezentat FIU-FRATE, implementarea parcurgerii în A-preordine se bazează pe următoarea procedură recursivă, având ca parametru de intrare rădăcina arborelui curent (vârful curent în momentul apelului). procedure A_preordine (R); if R≠0 then vizit (R); A_preordine(FIU[R]); A_preordine(FRATE[R]); endif; end;

B. Parcurgerea A-postordine Regula de vizitare a vârfurilor în parcurgerea în A-postordine diferă de cea în A-preordine numai prin faptul că rădăcina fiecărui arbore este vizitată după ce au fost vizitate toate celelalte vârfuri ale arborelui.

Exemplu: 4.9. Pentru arborele orientat din exemplul 4.7, ordinea de vizitare a vârfurilor este: 5,10,11,12,6,2,7,8,3,13,14,15,9,4,1. Pentru arbori reprezentaţi prin structuri de date arborescente, implementarea parcurgerii în A-postordine poate fi obţinută pe baza următoarei proceduri recursive. Unicul parametru (de intrare) reprezintă rădăcina arborelui curent în momentul apelului. procedure A_postordine (R); if R≠nil then do-for i=1,n,1 A_postordine(R^.leg[i]);

53

Structuri arborescente

enddo; vizit (R); endif; end; Procedurile A-preordine şi A-postordine sunt variante de parcurgeri în adâncime, fiind prioritare vârfurile aflate la distanţă maximă faţă de rădăcina arborelui iniţial.

C. Parcurgerea pe niveluri Definiţia 4.1.8. Un vârf v al unui arbore orientat cu rădăcină r se află pe nivelul i al arborelui, dacă distanţa de la vârf la rădăcină (lungimea r-v drumului) este egală cu i. Rădăcina arborelui este de nivel 0. Parcurgerea unui arbore orientat pe niveluri constă în vizitarea vârfurilor sale în ordinea crescătoare a distanţelor faţă de rădăcină. Exemplu: 4.10. Pentru arborele din exemplul 4.7, prin aplicarea parcurgerii pe niveluri, rezultă: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15. Implementarea parcurgerii pe niveluri se bazează pe utilizarea unei structuri de coadă C. La momentul iniţial, rădăcina arborelui este unicul element din C. Atâta timp cât coada este nevidă, se extrage (cu ştergere) un vârf din C, este vizitat şi sunt introduşi în coadă descendenţii săi. Calculul se încheie în momentul în care, la tentativa de extragere a unui vârf din C, se constată C=Ø. Parcurgerea pe niveluri este realizată de următoarea procedură care are ca parametri de intrare reprezentarea FIU-FRATE a grafului. Procedurile push şi pop realizează operaţiile de acces introducere-extragere în C. procedure parcurgere_pe_niveluri(R,FIU,FRATE,n) C:ptcoada; C=nil;push(C,R); while C≠nil do pop(C,v); VIZIT(v); v=FIU[v]; while v≠0 do push(C,v); v=FRATE[v]; endwhile; endwhile; end;

54

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Exemplu 4.11. Pentru arborele de la exemplul 4.7, evoluţia algoritmului este: C t t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8 t=9 t=10 t=11 t=12 t=13 t=14 t=15 t=16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

3 4 5 6 7 8 9 10 11 12 13 14 15

4 5 6 7 8 9 10 11 12 13 14 15

6 7 8 9 10 11 12 13 14 15

8 9 11 12

12

14 15

15

deci vârfurile sunt vizitate în ordinea: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15. Metoda BF pentru parcurgerea grafurilor este o generalizare a tehnicii de parcurgere pe niveluri a arborilor orientaţi. O alternativă de implementare a parcurgerii pe niveluri poate fi descrisă prin intermediul procedurilor recursive fraţi şi parc. Coada C este o variabilă globală şi este iniţializată cu rădăcina arborelui. Parcurgerea este obţinută prin apelul parc(C). procedure fraţi(v); if v≠0 then push(C,v);fraţi(FRATE[v]; endif; end; procedure parc; if C≠nil then pop(C,v);VIZIT(v); fraţi(FIU[v]); parc; endif; end;

55

Structuri arborescente

4.1.3 Arbori parţiali; algoritmul Kruskal Definiţia 4.1.9. Fie G graf. Subgraful parţial H este un arbore parţial al lui G dacă H este graf arbore. Definiţia 4.1.10. Fie (V,E,w) un graf ponderat conex. Dacă T=(V,E0) este un arbore parţial al grafului G=(V,E), ponderea arborelui T este definită prin: W(T)= w ( e) .



e∈E 0

Definiţia 4.1.11. Fie T(G) mulţimea arborilor parţiali corespunzători grafului G. T0∈T(G) este arbore parţial minim pentru G dacă W(T0)=min{W(T); T∈T(G)}. Dacă G este graf finit, atunci T(G) este mulţime finită, deci orice graf finit ponderat şi conex are cel puţin un arbore parţial minim. Pentru calculul unui arbore parţial minim sunt cunoscuţi mai mulţi algoritmi. În continuare este prezentat algoritmul Kruskal pentru determinarea unui arbore parţial minim al unui graf ponderat conex G=(V,E,w). Pasul 1: i=1; E0=∅ Pasul 2: Determină mulţimea R={e/e∈E \ Ei-1 astfel încât graful (V,Ei-1 ∪ {e}) este aciclic} Dacă R=∅, atunci stop; altfel, selectează ei∈R cu w(ei)=min{w(e), e∈R}; Ei=Ei-1 ∪ {ei} Pasul 3: i=i+1 şi reia pasul 2. Structura (V,Ei-1) calculată de procedură este arbore parţial minim al grafului conex ponderat G. Ideea algoritmului Kruskal revine la alegerea şi includerea în mulţimea de muchii curente a unei muchii de cost minim încă neselectate şi astfel încât să nu formeze un ciclu cu muchiile selectate la etapele precedente. Algoritmul se încheie atunci când nici o alegere nu mai este posibilă. Aplicarea metodei la grafuri neconexe calculează o mulţime de arbori parţiali minimi, câte un arbore pentru fiecare componentă conexă. Pentru implementarea algoritmului Kruskal, graful conex ponderat este reprezentat sub formă tabelară, muchiile fiind ordonate crescător după ponderi. Muchiile selectate de algoritm pot fi menţinute, de asemenea, într-o structură tabelară, sau doar marcate ca fiind incluse în mulţimea muchiilor arborelui parţial minim a cărui construcţie este dorită. În varianta prezentată în continuare muchiile selectate sunt afişate. Pentru verificarea condiţiei ca muchia selectată să nu formeze nici un ciclu cu muchiile selectate la etapele precedente, este utilizat un vector, TATA. Pentru fiecare vârf i (vârfurile grafului fiind numerotate de la 1 la n, unde n este numărul nodurilor grafului), componenta TATA [i] este predecesorul său în arborele care conţine vârful i construit până la momentul curent, dacă i nu este rădăcina acelui

56

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

arbore, respectiv TATA[i] este egal cu –numărul de vârfuri ale arborelui de rădăcină i, în caz contrar. Componentele vectorului TATA sunt iniţializate cu valoarea -1. Calculul care realizează adăugarea unei noi muchii poate fi descris astfel: - este determinată o muchie de cost minim, e=v1v2, care nu a fost selectată anterior; - se verifică proprietatea de aciclicitate a grafului care rezultă prin eventuala adăugare a muchiei selectate astfel: dacă vârfurile v1 şi v2 nu aparţin aceluiaşi arbore, atunci proprietatea de aciclicitate este îndeplinită şi muchia e este adăugată la structura curentă. Determinarea valorii k reprezentând rădăcina arborelui care conţine vârful dat v rezultă prin parcurgerea vectorului TATA: k=v; while TATA[k] >0 do k=TATA[k] endwhile; - adăugarea muchiei e selectate este realizată prin reunirea arborilor cu rădăcini r1 şi r2, din care fac parte v1 şi respectiv v2, astfel: dacă TATA[r1]
4.2 Arbori binari 4.2.1 Reprezentare; parcurgeri Definiţia 4.2.1. Un arbore binar este un arbore orientat cu proprietatea că pentru orice vârf v, od(v)≤2. În cazul od(v)=2, cei doi descendenţi sunt desemnaţi ca descendent stâng (fiu stânga) respectiv descendent drept (fiu dreapta). Pentru vârfurile cu od(v)=1, unicul descendent este specificat fie ca fiu stânga, fie ca fiu dreapta. Definiţia 4.2.2. Se numeşte nod terminal orice vârf v al arborelui cu od(v)=0. Nodul v este neterminal dacă od(v)>0. Reprezentarea unui arbore binar este realizată prin reţinerea, pentru fiecare nod, a legăturilor către descendenţii lui. Absenţa unui descendent este reprezentată prin nil.

57

Structuri arborescente

identificator nod

legătură fiu stânga

legătură fiu dreapta

Structura de date Pascal este: type arb=^nod; nod=record; inf:integer; fius,fiud:arb; end;

Definiţia 4.2.3. Fie T=(V,E) un arbore binar cu rădăcina R. Subarborele stâng al lui T este ST=(V\{R},E\{RS}), unde S este fiul stânga al rădăcinii. Subarborele drept al lui T este DT=(V\{R},E\{RD}), unde D este fiul dreapta al rădăcinii. În plus faţă de metodele deja prezentate pentru parcurgerea arborilor generali şi care sunt aplicabile şi în acest caz particular, parcurgerile în preordine(RSD), inordine(SRD) şi respectiv postordine(SDR) sunt special considerate pentru arbori binari şi au multiple aplicaţii. Regula de vizitare revine la parcurgerea subarborelui stâng, a subarborelui drept corespunzători vârfului curent. La momentul iniţial, vârful curent este rădăcina arborelui. Diferenţa între cele trei tipuri de parcurgere este dată de momentul în care este vizitat fiecare vârf al arborelui. În parcurgerea RSD (rădăcină-subarbore stâng-subarbore drept), fiecare vârf al arborelui este vizitat în momentul în care devine vârf curent; în parcurgerea SRD (subarbore stâng-rădăcinăsubarbore drept), vizitarea vârfului este efectuată după ce a fost parcurs subarborele stâng; în parcurgerea SDR (subarbore stâng-subarbore drept-rădăcină) vizitarea fiecărui vârf este efectuată după ce au fost parcurşi subarborii aferenţi lui. Procedura preordine realizează parcurgerea în preordine, metodele SRD şi SDR fiind propuse cititorului ca exerciţii. Procedura preordine este recursivă şi are ca parametru de intrare vârful curent în momentul apelului. procedure preordine(r:arb); begin if r<>nil then begin write(r^.inf); preordine(r^.fius); preordine(r^.fiud); end; end;

58

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

4.2.2 Arbori binari de sortare Definiţia 4.2.4. Un arbore de sortare este un arbore binar cu proprietăţile: ⇒ fiecărui nod i al arborelui îi este ataşată o informaţie INF(i) dintr-o mulţime ordonată de valori; ⇒ pentru fiecare nod i, INF(i) este mai mare decât INF(j), pentru toate nodurile j din subarborele stâng al arborelui cu rădăcină i; ⇒ pentru fiecare nod i, INF(i) este mai mică decât INF(j), pentru toate nodurile j din subarborele drept al arborelui cu rădăcină i; ⇒ pentru orice vârfuri i,j, dacă i≠j, atunci INF(i)≠INF(j). Exemplu: 4.12. Arborele binar

50 30

20

70

40

60

90

80

este de sortare. Operaţiile uzuale efectuate asupra arborilor de sortare sunt inserarea unui nod, ştergerea unui nod şi parcurgerea arborelui (în preordine, inordine sau postordine). Inserarea şi ştergerea nodurilor într-un arbore de sortare trebuie realizate astfel încât arborele rezultat să fie, de asemenea, arbore de sortare. Parcurgerea în inordine a unui arbore de sortare determină secvenţa vârfurilor arborelui în ordinea crescătoare a informaţiilor ataşate.

Exemplu: 4.13. Pentru arborele de sortare descris în exemplul 4.12, parcurgerea în inordine determină secvenţa de valori: 20,30,40,50,60,70,80,90, adică exact vectorul de informaţii asociate nodurilor ordonat crescător.

59

Structuri arborescente

Inserarea unui nod într-un arbore de sortare Algoritmul de inserare a unei informaţii nr în arborele de sortare de rădăcină rad este recursiv şi constă în efectuarea operaţiilor. Vârful curent v la momentul iniţial este rădăcina arborelui. - 1. dacă arborele de rădăcină v este vid (v=nil), este generat arborele cu un singur nod, având nr ca informaţie ataşată; - 2. altfel - a) dacă informaţia ataşată nodului v este mai mare decât nr, atunci vârful curent devine fiul stânga al lui v; - b) dacă informaţia ataşată nodului v este egală cu nr, atunci stop (se previne duplicarea informaţiilor ataşate vârfurilor arborelui); - c) dacă informaţia ataşată nodului v este mai mică decât nr, atunci vârful curent devine fiul dreapta al lui v.

Exemplu: 4.14. Aplicarea algoritmul descris pentru inserarea informaţiei 55 în arborele de sortare din exemplul 4.12 determină următoarele operaţii: INF(v)=50<55: se decide inserarea în subarborele drept cu rădăcina având informaţia ataşată 70 (cazul 2.c); INF(v)=70>55: se decide inserarea în subarborele stâng cu rădăcina având informaţia ataşată 60 (cazul 2.a); INF(v)=60>55: se decide inserarea în subarborele stâng cu rădăcina nil (situaţia de la 1). Se decide crearea nodului cu informaţie 55, fiu stâng al nodului de informaţie 60. Arborele rezultat este:

50 30

20

70

40

60

55

90

80

60

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Ştergerea unei informaţii dintr-un arbore de sortare Algoritmul pentru ştergerea unei informaţii nr din arborele de sortare de rădăcină rad este recursiv este descris în continuare. Vârful curent v la momentul iniţial este rădăcina arborelui. - 1. dacă arborele este vid (v=nil) atunci stop (nr nu se află în mulţimea informaţiilor ataşate nodurilor arborelui); - 2. altfel - a) dacă informaţia ataşată nodului v este mai mare decât nr, atunci vârful curent devine fiul stânga al lui v ; - b) dacă informaţia ataşată nodului v este mai mică decât nr, vârful curent devine fiul dreapta al lui v ; - c) dacă INF(v)=nr atunci: - c1) dacă subarborele stâng este vid (v^.fius=nil), atunci adresa vârfului v este memorată într-o celulă suplimentară aux, v devine fiul dreapta al lui v, iar celula aux este eliberată din memorie (este disponibilizată celula corespunzătoare vârfului din arbore de informaţie nr); - c2) dacă subarborele stâng este nevid, atunci se determină cel mai mare element din subarborele stâng (este parcurs subarborele stâng pe legăturile din dreapta, cât timp acest lucru este posibil, cu păstrarea şi a adresei nodului părinte corespunzător fiecărui vârf atins) : p:=v^.fius; while p^.fiud<> nil do begin p1:=p; p:=p^.fiud; end; c2.1) dacă fiul stânga al lui v nu are subarbore drept (v^.fius^.fiud=nil), atunci informaţia ataşată fiului stânga se transferă în vârful curent, iar fiul stânga (v^.fius) este înlocuit cu fiul său stânga (v^.fius^.fius) şi este eliberată memoria corespunzătoare celulei v^.fius. c2.2) altfel, se transferă în rădăcină informaţia ataşată ultimului nod p determinat la c2), nodul p este înlocuit cu fiul său stâng şi celula corespunzătoare lui p este eliberată din memorie.

Exemple: 4.15. Ştergerea informaţiei 70 în arborele de sortare din exemplul 4.12 este realizată astfel: 70>50, decide ştergerea din subarborele drept (cu rădăcină 70) - situaţia 2.b; 70=70, decide ştergerea din arborele curent: rădăcina etichetată cu 70 situaţia 2.c;

61

Structuri arborescente

Există subarbore stâng (rădăcina lui p este etichetată cu 60) iar acesta nu are subarbore drept - situaţia 2.c.1.: nodul cu informaţie 70 este etichetat cu 60, iar p este înlocuit cu subarborele său stâng (vid) Arborele de sortare rezultat este:

50 30

20

60

40

90

80 4.16. Ştergerea informaţiei 30 din arborele de sortare:

50 30

20

10

70

40

60

90

25

24

este realizată astfel: 30>50, decide ştergerea din subarborele stâng (cu rădăcină 30) - situaţia 2.a; 30=30, decide ştergerea din arborele curent: rădăcina etichetată cu 70 – situaţia 2.c;

62

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Există subarbore stâng (rădăcina este etichetată cu 20), iar acesta are subarbore drept - situaţia 2.c.2: nodul cu informaţie 30 este etichetat cu 25 (informaţia ultimului nod p detectat la c2)), iar p este înlocuit cu subarborele său stâng (cu rădăcină 24). Arborele rezultat este:

50 25

20

10

70

40

60

90

24

Punctul c) de la pasul 2 poate fi înlocuit cu: c) dacă INF(v)=nr atunci: - c1) dacă subarborele drept este vid (v^.fiud=nil), atunci adresa vârfului v este memorată într-o celulă suplimentară aux, v devine fiul stânga al lui v, iar celula aux este eliberată din memorie (este eliberat vârful de informaţie nr); - c2) dacă subarborele drept este nevid, atunci se determină cel mai mic element din subarborele drept (este parcurs subarborele drept pe legăturile din stânga, cât timp acest lucru este posibil, cu păstrarea şi a adresei nodului părinte corespunzător fiecărui vârf atins) : p:=v^.fiud; while p^.fius<> nil do begin p1:=p;p:=p^.fius; end; c2.1.) dacă fiul dreapta al lui v nu are subarbore stâng (v^.fiud^.fius=nil), atunci informaţia ataşată fiului dreapta se transferă în vârful curent, iar fiul dreapta este înlocuit cu fiul său dreapta (v^.fiud^.fiud) şi este eliberată memoria corespunzătoare celulei v^.fiud. c2.2) altfel, se transferă în rădăcină informaţia ataşată ultimului nod p determinat la c2), nodul p este înlocuit cu fiul său dreapta şi celula corespunzătoare lui p este eliberată din memorie.

63

Structuri arborescente

4.2.3 Arbori de structură Expresiile aritmetice în care intervin numai operatori binari pot fi reprezentate prin intermediul arborilor strict binari (fiecare nod neterminal are doi fii). Un arbore de structură are vârfurile etichetate astfel: - fiecare nod neterminal este etichetat cu un simbol corespunzător unuia dintre operatori; - fiecare nod terminal este etichetat cu un operand (variabilă sau constantă); Construcţia arborelui de structură corespunzător unei expresii aritmetice date se realizează pe baza “parantezării” existente în expresie şi a priorităţilor convenţional asociate operatorilor (ordinea operaţiilor) astfel încât rădăcina fiecărui subarbore este etichetată cu operatorul care se execută ultimul în evaluarea subexpresiei corespunzătoare acelui subarbore.

Exemplu: 4.17. Pentru expresia matematică (a+b)*(c-d)+e/g, arborele de structură este:

+ ∗

/

+

a



b

c

e

g

d

Construcţia arborelui de structură pentru o expresie s se face în două etape, şi anume: 1. Ataşarea priorităţilor operatorilor şi operanzilor (toţi operanzii au aceeaşi prioritate, egală cu prioritatea maximă). Priorităţile ataşate permit eliminarea parantezelor fără ca semnificaţia expresiei să se modifice; 2. Construcţia propriu-zisă.

64

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Prima etapă este realizată astfel: prioritatea iniţială a operatorilor ‘+’,’-‘ este 1 (dacă expresia nu conţine paranteze, atunci în construcţie operatorii vor fi primii luaţi în considerare, în ordinea de la dreapta la stânga); - prioritatea iniţială a operatorilor ‘/’,’*‘ este 10 (dacă expresia nu conţine paranteze, aceştia sunt consideraţi după operatorii de prioritate 1 în ordinea de la dreapta la stânga); - prioritatea fiecărui operator este incrementată cu valoarea 10 pentru fiecare pereche de paranteze în interiorul cărora se află; - prioritatea ataşată fiecărui operand este maxint. După stabilirea sistemului de priorităţi, se elimină parantezele din expresie (ordinea de efectuare a operaţiilor în cadrul expresiei este indicată de vectorul de priorităţi ataşat). -

Exemplu: 4.18. Etapele calculului sistemului de priorităţi pentru expresia de la exemplul 4.17 sunt: i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

j 10 10 10 10 0 0 10 10 10 10 0 0 0 0 0

dim 0 1 2 3 3 4 4 5 6 7 7 8 9 10 11

vectorul prioritate (maxint) (maxint,11) (maxint,11,maxint) (maxint,11,maxint) (maxint,11,maxint,10) (maxint,11,maxint,10) (maxint,11,maxint,10,maxint) (maxint,11,maxint,10,maxint,11) (maxint,11,maxint,10,maxint,11,maxint) (maxint,11,maxint,10,maxint,11,maxint) (maxint,11,maxint,10,maxint,11,maxint,1) (maxint,11,maxint,10,maxint,11,maxint,1,maxint) (maxint,11,maxint,10,maxint,11,maxint,1,maxint,10) (maxint,11,maxint,10,maxint,11,maxint,1,maxint,10,maxint)

După eliminarea parantezelor, expresia rezultată este s=a+b*c-d+e/g, având ca vector de priorităţi (maxint,11,maxint,10,maxint,11,maxint,1,maxint,10,maxint). Construcţia arborelui de structură pe baza expresiei s, din care au fost eliminate parantezele şi a vectorului de priorităţi, poate fi realizează recursiv în modul descris în continuare. La momentul iniţial expresia curentă este cea dată.

65

Structuri arborescente

-

-

Pentru expresia curentă se determină operatorul/operandul de prioritate minimă care se ataşează ca etichetă a rădăcinii r a subarborelui de structură corespunzător ei; fie i poziţia acestuia în cadrul expresiei; Dacă expresia are un singur simbol (operand) atunci r^.fius=r^.fiud=nil; Altfel, se consideră subexpresiile s1 şi s2, constând din simbolurile de pe poziţiile 1 până la i-1 şi respectiv i+1 până la length(s). Arborii de structură corespunzători subexpresiilor s1 şi s2 se ataşează ca subarbore stâng, respectiv subarbore drept vârfului r.

Exemplu: 4.19. Pentru expresia de la exemplul 4.17, după determinarea vectorului de priorităţi şi a expresiei neparantezate corespunzătoare, procedura cr_der realizează următoarea construcţie: s=a+b*c-d+e/g, prioritate=(maxint,11,maxint,10,maxint,11,maxint,1,maxint,10,maxint) p=1,u=11 ⇒ min=1, i=8, arborele:

+

în construcţie

în construcţie

p=1,u=7 ⇒ min=10, i=4, arborele:

+

*

în construcţie

în construcţie în construcţie

66

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

p=1,u=3 ⇒ min=11, i=2, arborele: +



în construcţie

în construcţie

+

în construcţie

în construcţie

p=u=1 ⇒ min=maxint, i=1, arborele:

+



în construcţie

+

a

în construcţie

în construcţie

67

Structuri arborescente

p=3,u=3 ⇒min=maxint, i=3, arborele: +



în construcţie

în construcţie

+

a

b

p=5,u=7 ⇒ min=11, i=6, arborele: +

în construcţie



+

a



b

în construcţie

în construcţie

68

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

p=5,u=5 ⇒ min=maxint, i=5, arborele: +

în construcţie



+



a

b

în construcţie

c

p=7,u=7 ⇒ min=maxint, i=7, arborele:

+

în construcţie



+

a



b

c

d

69

Structuri arborescente

p=9,u=11 ⇒ min=10,i=10, arborele:

+



/

+

b

a

în construcţie



în construcţie

d

c

p=9,u=9 ⇒ min=maxint,i=9, arborele:

+



/

+

a



b

c

e

în construcţie

d

70

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

p=11,u=11 ⇒ min=maxint,i=11, arborele: +



/

+

a



b

c

e

g

d

şi construcţia se termină. Construcţia arborelui de structură se face în ipoteza în care expresia este corectă. Procedura cr_der se apelează cu:nil,1,length(s),s,prioritate, unde s şi prioritate sunt cei rezultaţi după apelarea procedurii priorităţi.

Definiţia 4.2.5. Se numeşte forma poloneză directă a unei expresii, expresia rezultată în urma parcurgerii RSD a arborelui de structură. . Se numeşte forma poloneză inversă a unei expresii, expresia rezultată în urma parcurgerii SDR a arborelui de structură. Exemplu: 4.20. Pentru expresia considerată la exemplul 4.17, forma poloneză directă este +*+ab-cd/eg. Forma poloneză inversă a expresiei date este ab+cd-*eg/+. Parcurgerea arborelui în inordine determină secvenţa de simboluri rezultată prin eliminarea parantezelor din expresia dată. Restaurarea unei forme parantezate poate fi realizată printr-o parcurgere SRD în modul următor. La momentul iniţial, vârful curent este rădăcina arborelui de structură. Dacă vârful curent v nu este vârf terminal, atunci se generează (s1) eticheta(v) (s2), unde eticheta(v) este operatorul etichetă a vârfului, s1 este secvenţa rezultată prin traversarea SRD a subarborelui stâng, s2 este secvenţa rezultată prin traversarea SRD a subarborelui drept. Dacă v este vârf terminal, atunci este generată secvenţa eticheta(v).

71

Structuri arborescente

Exemplu: 4.21. Prin aplicarea traversării SRD a arborelui de structură al expresiei din 4.2.6, rezultă: s=(((a)+(b))*((c)-(d)))+((e)/(g)). Se observă că, prin aplicarea traversării SRD şi a parantezării descrise, expresia rezultată are aceeaşi semnificaţie cu expresia iniţială, dar apar multe paranteze “inutile”. Propunem ca exerciţiu scrierea unei proceduri Pascal pentru eliminarea parantezelor inutile. Evaluarea expresiilor aritmetice pe baza arborilor de structură Traversarea SRD a arborelui de structură ataşat unei expresii aritmetice permite evaluarea expresiei pentru valorile curente corespunzătoare variabilelor. Evaluarea poate fi efectuată în mod recursiv. La momentul iniţial, vârful curent este rădăcina arborelui. Dacă v este vârf curent, atunci noua informaţie asociată lui v este: - val(eticheta(v)), dacă v este vârf terminal; - val(s1)eticheta(v)val(s2), dacă v este neterminal, unde val(s1), val(s2) sunt valorile rezultate prin evaluările subarborilor stâng şi respectiv drept ai lui v; val(eticheta(v)) este valoarea curentă a variabilei, dacă eticheta lui v este variabilă, respectiv valoarea constantei, dacă eticheta lui v este o constantă. Dacă v este vârf neterminal, atunci noua informaţie asociată lui v este val(s1)eticheta(v)val(s2), ce reprezintă rezultatul operaţiei eticheta(v) aplicată valorilor val(s1), val(s2).

Exemplu: 4.22. Prin aplicarea metodei de evaluare descrise, se obţine: 18

15

3

5

3

3

2

5

6

2

2

72

ALGORITMI RECURSIVI METODELE DIVIDE ET IMPERA ŞI BACKTRACKING

Recursivitatea este o tehnică de programare bazată pe apelarea unui subprogram de către el însuşi. În cadrul capitolului sunt prezentate calculul recursiv, metoda „divide et impera” şi metoda backtracking implementată recursiv.

5.1 Calcul recursiv Calculul valorii n! pentru n dat poate fi efectuat pe baza formulei n!=n(n-1)!, pentru n ≥ 1 şi 0!=1. Dacă Fact(n) este funcţia Pascal care calculează n!, atunci, dacă n ≥ 1 evaluarea lui Fact(n) rezultă prin multiplicarea cu n a valorii calculate de apelul Fact(n-1), cu Fact(0)=1. Cu alte cuvinte, apelul funcţiei Fact(n) realizează calculul “imediat” dacă n=0, altfel presupune un nou apel al aceleiaşi funcţii pentru valoarea argumentului egală cu n-1. Cazurile în care este posibilă evaluarea “imediată” se numesc condiţii terminale. În Pascal, funcţia Fact, este: function Fact(n:byte):word; begin if n=0 then Fact:=1 else Fact:=n*Fact(n-1); end;

n! pentru calculul combinărilor (n, k date) k!(n − k )! ridică dificultăţi deoarece n!, pentru n ≥ 13, nu poate fi reprezentat în calculator ca dată de un tip întreg, chiar dacă numărul C nk este relativ mic şi poate fi reprezentat Utilizarea formulei C kn =

ca întreg. Pe baza relaţiei de recurenţă C nk = C nk−1 + C nk−−11 rezultă ceea ce este cunoscut sub numele de triunghiul lui Pascal. De exemplu, triunghiul lui Pascal, pentru n = 7, este:

73

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

1 1 1 1 1 1 1 1

7

2 3

4 5

6

1 3

6 10

15 21

1

4 10

20 35

1 1 5 15

35

1 6

21

1 7

1

Secvenţa de program pentru calculul C nk , 0 ≤ k ≤ n ≤ 13 este: var n,i,j:word; x:array[0..13,0..13] of word; begin write(‘Valoarea pentru n=’); readln(n); if n>13 then writeln(‘Eroare’) else begin x[0,0]:=1; for i:=1 to n do begin x[i,0]:=1; x[i,i]:=1; end; for i:=1 to n-1 do for j:=1 to i do x[i+1,j]= x[i,j-1]+x[i,j]; end; for i:=0 to n do begin for j:=0 to i do write(x[i,j],’ ‘); writeln; end; end.

Valorile combinărilor sunt calculate în componentele tabelei x, fiind utilizată numai

(n + 1)(n + 2) din cele 142 celule de memorie rezervate. 2

74

Algoritmi recursivi. Metodele divide et impera şi backtracking

Se poate proceda, însă, altfel. Se presupune că function comb(n,k) calculează

C nk . Conform relaţiei de recurenţă, dacă n ≥ k ≥ 1, atunci evaluarea corespunzătoare apelului comb(n,k) revine la însumarea rezultatelor obţinute prin apelurile comb(n1,k) şi comb(n-1, k-1), unde comb(n,0)=1, n ≥ 0. Dacă evaluările comb(n-1,k) şi comb(n-1, k-1) sunt realizate în acelaşi mod, rezultă că apelul comb(n,k) va determina o secvenţă de apeluri ale aceleiaşi funcţii pentru valori ale argumentelor din ce în ce mai mici, până când este îndeplinită una din condiţiile terminale comb(n,0)=1, comb(k,k)=1. Soluţia recursivă a evaluării C nk este: function comb(n,k:byte):word; begin if k>n then comb:=0 else if (k=0) or (k=n) then comb:=1 else comb:=comb(n-1,k)+comb(n-1,k-1); end;

Un alt exemplu este următorul. Se presupune că f0, f1, α , β sunt numere reale date şi pentru p ≥ 2, f p = αf p −1 + βf p − 2 . Se cere să se calculeze fn pentru n dat. Şirul definit de relaţia precedentă pentru f0=f1= α = β =1 se numeşte şirul lui Fibonacci. Dacă Fib(n) este funcţia Pascal care calculează cel de-al n-lea element din şirul considerat, atunci evaluarea lui Fib(n) revine la însumarea valorilor lui Fib(n-1) şi Fib(n-2) ponderate de constantele α şi β , adică rezolvarea problemei Fib(n) poate fi redusă la rezolvarea problemelor Fib(n-1), Fib(n-2) cu condiţiile terminale Fib(0)=f0, Fib(1)=f1. Funcţia Pascal Fib pentru calculul celui de-al n-lea termen al şirului definit anterior este: function Fib(n:word;alfa, beta, f0,f1:real):real; begin if n=0 then Fib:=f0 else if n=1 then Fib:=f1 else Fib:=alfa*Fib(n-1)+beta*Fib(n-2); end;

Fiecare apel Fib(n) pentru n>1 determină încă două apeluri: Fib(n-1) şi Fib(n-2). Numărul total de apeluri efectuate până la rezolvarea problemei Fib(n) creşte exponenţial, în funcţie de parametrul n. Mai mult, rezolvarea problemei Fib(n-2) are loc atât la apelul Fib(n-1), cât şi la apelul determinat de Fib(n). Datorită acestor inconveniente, este preferată o soluţie iterativă pentru calculul unui termen de rang dat al şirului lui Fibonacci.

75

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Se spune că astfel de apeluri sunt recursive directe. Schema unui apel recursiv poate fi descrisă în modul următor. Se verifică dacă este îndeplinită cel puţin una din condiţiile terminale. Dacă este îndeplinită o condiţie terminală, atunci calculul se încheie şi se revine la unitatea apelantă. În caz contrar, este iniţiat calculul pentru noile valori ale parametrilor, calcul care presupune unul sau mai multe apeluri recursive. Mecanismul prin care este efectuat apelul unui subprogram se bazează pe utilizarea stivei memoriei calculatorului. Fiecare apel determină introducerea în stivă (operaţia push) a valorilor/adreselor parametrilor formali, adresei de revenire şi a variabilelor locale. La momentul execuţiei, aceste informaţii sunt extrase cu eliminare din stivă (operaţia pop), eliberându-se spaţiul ocupat. În cazul subprogramelor recursive, mecanismul funcţionează astfel: se generează un număr de apeluri succesive cu ocuparea spaţiului din stivă necesar efectuării acestor apeluri până la îndeplinirea unei condiţii terminale; apelurile sunt executate în ordinea inversă celei în care au fost generate, iar operaţia push poate produce depăşirea spaţiului de memorie rezervat în stivă. Astfel, în cazul apelului Fact(3), secvenţa de apeluri recursive iniţiate este: Fact(2), Fact(1), Fact(0). În continuare execuţia determină Fact(0)=1, Fact(1)=1*Fact(0)=1, Fact(2)=2*Fact(1)=2, Fact(3)=3*Fact(2)=6. Evoluţia determinată de apelul Fact(3) în stivă este ilustrată în figurile 5.1 şi 5.2, unde (○) reprezintă adresa de revenire în punctul de unde a fost efectuat apelul Fact(3). Apelurile recursive ale unei proceduri sau funcţii pot fi şi indirecte, în sensul că este efectuat un apel al unei alte proceduri sau funcţii care, la rândul ei, iniţiază un apel al procedurii sau funcţiei iniţiale. Un exemplu simplu îl reprezintă calculul valorilor funcţiei h=f◦g◦f , unde f,g:R→R sunt funcţii date.Un mod de a rezolva această problemă este descris în continuare. Dacă f(x), g(x) sunt funcţiile Pascal care descriu calculul necesar evaluării funcţiilor date în punctul x, atunci pentru calculul valorii h(x) este necesar apelul funcţiei f , urmat de apelul funcţiei g care, la rândul ei, apelează din nou funcţia f. Pentru funcţiile f, g definite prin

2 x + 1, x < 3 x 2 − 3x + 2, x ≤ 1 f (x) =  2 , g( x ) =  x + 2, x ≥ 3 3x + 5, x > 1 funcţia Pascal h(x) este:

76

Algoritmi recursivi. Metodele divide et impera şi backtracking 3

2

Fact=3*Fact(2)

Fact=2*Fact(1)

Adresa de revenire

Adresa de revenire 3 Fact=3*Fact(2) (o)

Adresa de revenire

1

0

Fact=1*Fact(0)

Fact=1

Adresa de revenire

Adresa de revenire

2

1

Fact=2*Fact(1)

Fact=1*Fact(0)

Adresa de revenire

Adresa de revenire

3

2

Fact=3*Fact(2)

Fact=2*Fact(1)

Adresa de revenire

Adresa de revenire

(o)

3 (o)

Fact=3*Fact(2) Adresa de revenire

(o)

Fig. 5.1 Evoluţia în stivă până la condiţia terminală Fact(0):=1

77

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

1

2

Fact=1

Fact=2

Adresa de revenire

Adresa de revenire

2

3

Fact=2*Fact(1)

Fact=3*Fact(2)

Adresa de revenire

Adresa de revenire

3

(○)

3

Fact=3*Fact(2) Adresa de revenire

Fact=6

(○) Adresa de revenire

(○)

Stiva vidă Fig. 5.2 Eliberarea stivei după execuţia determinată de condiţia terminală function f(x:real):real; begin if x<3 then f:=2*x+1 else f:=x*x+2 end; function g(x:real):real; begin if x<=1 then g:=x*x-3*x+2 else g:=3*x+5 end; function h(x:real):real; begin h:=f(g(f(x))); end;

78

Algoritmi recursivi. Metodele divide et impera şi backtracking

5.2 Metoda “divide et impera” Metoda “divide et impera” presupune descompunerea problemei de rezolvat în două sau mai multe subprobleme (probleme “mai simple”), determinarea soluţiilor acestora care, apoi, compuse după reguli simple, furnizează soluţia problemei iniţiale. De exemplu, se presupune că se doreşte aflarea valorii maxime dintr-o secvenţă {a 1 ,..., a n } de n numere. Pentru rezolvarea problemei se poate proceda în mai multe moduri: se determină valoarea cea mai mare din prima jumătate, fie aceasta x1, apoi se determină valoarea cea mai mare din a doua jumătate a secvenţei, fie aceasta x2. Soluţia problemei este max(x1, x2). Problema iniţială a fost descompusă în două subprobleme de acelaşi tip, dar “mai simple” deoarece lungimea fiecărei secvenţe este jumătate din lungimea secvenţei iniţiale. Problema poate fi rezolvată pe baza unei metode care o “reduce” succesiv la o problemă “mai simplă”, determinându-se valoarea maximă din primele n-1 componente ale secvenţei (fie aceasta x1), valoarea maximă fiind max(x1,an). Ambele soluţii sunt recursive şi sunt reprezentate prin funcţiile Pascal max1 şi max2. function max1(var a:vector; s,d:byte):real; var x1, x2:real; begin if s=d then max1=a[s] else begin x1:=max1(a,s,(s+d)div 2); x2:=max1(a,(s+d)div 2+1,d); if x1>x2 then max1:=x1 else max1:=x2; end; end; function max2(var a:vector; n:byte):real; var x1:real; begin if n=1 then max2=a[1] else begin x1:=max1(a,n-1); if x1>a[n] then max1:=x1 else max1:=a[n]; end; end;

Un alt exemplu în care, pentru rezolvarea problemei, se poate efectua un raţionament similar este următorul. Se presupune că ecuaţia f(x)=0 are o singură soluţie x0 în intervalul (a,b). Se doreşte obţinerea unei valori aproximative ˆx astfel încât x0 − ˆx < ε , pentru ε > 0 dat. Deoarece ecuaţia f(x)=0 are o singură soluţie x0 în intervalul [a,b], rezultă că f(a)f(b)<0 şi, de asemenea, dacă pentru a<α<β
79

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

intervalului (a,b), atunci este îndeplinită una şi numai una dintre relaţiile f(a)f(c)<0, f(c)=0, f(b)f(c)<0. Dacă f(c)=0, atunci x0=c. Dacă f(a)f(c)<0 atunci x 0 ∈ (a , c) , altfel x 0 ∈ (c, b) . Se presupune că f(a)f(c)<0. Se poate aplica acelaşi procedeu intervalului (a,c) şi se continuă până când sau este obţinută soluţia exactă sau intervalul care conţine soluţia este de lungime inferioară lui ε . În cazul în care terminarea calculului se realizează prin identificarea unui interval de lungime inferioară lui ε , ˆx poate fi considerat oricare dintre numerele din acel interval, de exemplu mijlocul intervalului. Evident, numărul maxim de iteraţii N pentru obţinerea preciziei ε rezultă din inegalitatea

 b−a  b − a  < ε , adică N = log 2   + 1 . N 2  ε  

Se presupune că funcţia f este calculată prin apelul funcţiei Pascal f(a:real):real. O variantă recursivă a metodei descrise este: uses crt; {$F+} type fct=function(x:real):real; var eps,a,b,x:real; functie:fct; function f(x:real):real; begin f:=x*x*x-8; end; procedure bisectie(a,b,eps:real;var f:fct;var x:real); var xx:real; begin if f(a)=0 then x:=a else if f(b)=0 then x:=b else if b-a<eps then x:=(a+b)/2 else begin xx:=(a+b)/2; if f(xx)*f(a)<0 then bisectie(a,xx,eps,f,x) else bisectie(xx,b,eps,f,x) end; end; begin clrscr; write('Introduceti a '); readln(a); write('Introduceti b '); readln(b); eps:=exp(-20); functie:=f; bisectie(a,b,eps,functie,x); writeln('Solutia este:',x:5:2); readln; end.

80

Algoritmi recursivi. Metodele divide et impera şi backtracking

De asemenea, rezolvarea problemei turnurilor din Hanoi a fost realizată tot pe baza unei metode de reducere şi anume: problema deplasării a n discuri P(n) a fost redusă succesiv la rezolvarea problemelor mai simple P(n-1), P(n-2),…,P(1). Unul dintre cei mai eficienţi algoritmi pentru sortarea crescătoare a unei secvenţe de numere reale este cunoscut sub numele de algoritmul de quicksort (sortare rapidă) şi reprezintă, de asemenea, un exemplu de aplicare a metodei divide et impera. Fie secvenţa (vp , vp+1 , …,vu ), unde iniţial p=1, u=n (n=dimensiunea vectorului). Dacă p = u, secvenţa este sortată. Altfel, se poziţionează vp în această secvenţă astfel încât toate elementele ce ajung în faţa lui să fie mai mici decât el şi toate cele care îi urmează să fie mai mari decât el; fie poz poziţia lui corectă în secvenţa (vp , vp+1 , …, vpoz , vpoz+1 , …,vn). Procedeul se reia pentru secvenţele (vp , vp+1 , …,v poz-1) şi (vpoz+1, vpoz+2, …,vn), deci p ≤ poz-1 şi poz+1 ≤ u. Poziţionarea elementului vp se face astfel: se utilizează doi indicatori, i şi j; iniţial i = p şi j = u. Se compară vi cu vj, dacă nu este necesară interschimbarea, se micşorează j cu 1, repetându-se procesul; dacă apare o interschimbare, se măreşte i cu 1 şi se continuă compararea, mărind i până la apariţia unei noi interschimbări. Apoi se micşorează din nou j, continuându-se în acelaşi mod până când i = j.

Exemplu: 5.1. Aplicarea algoritmului de sortare rapidă pentru v = (10, 12, 19, 15, 3, 17, 4, 18) 1 2 3 4 5 6 7 8 p = 1, u = n = 8 a) poziţionarea lui vp = v1 = 10 i=1, j=8 Sensul de parcurgere: ← i=1, j=8 10<18 ⇒ j=j-1 i=1,j=7 10>4 ⇒ se efectuează interschimbarea şi se atribuie i=i+1=2 v=(4, 12, 19, 15, 3, 17, 10, 18) i=2, j=7 Sensul de parcurgere: → i=2, j=7 12>10 ⇒ se efectuează interschimbarea şi se atribuie j=j-1=6 v=(4, 10, 19, 15, 3, 17, 12, 18) i=2, j=6 Sensul de parcurgere: ← i=2, j=6 10<17 ⇒ j=j-1 i=2, j=5 10>3 ⇒ se efectuează interschimbarea şi se atribuie i=i+1=3 v = (4, 3, 19, 15, 10, 17, 12, 18) i=3, j=5 Sensul de parcurgere: → i=3, j=5 19>10 ⇒ se efectuează interschimbarea şi se atribuie j=j-1=4

81

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

v=(4, 3, 10, 15, 19, 17, 12, 18) i=3, j=4 Sensul de parcurgere: ← i=3, j=4 10<15 ⇒ j=j-1 i=3, j=3 i=j, poziţionare realizată b) Se obţine vectorul: v=(4, 3, 10, 15, 19, 17, 12, 18) 1 2 3 4 5 6 7 8 poz=3, se lucrează cu secvenţele (4, 3) şi (15, 19, 17, 12, 18) Primul nivel de recursie: Pentru Sv1=(4, 3) p=1, u=2 a1) poziţionarea lui 4: i=1, j=2 Sensul de parcurgere: ← i=1,j=2 4>3 ⇒ se efectuează interschimbarea şi se atribuie i=i+1=2 sv1 (3, 4) , poz=2 i=j=2, stop b1) se ajunge la scevenţele (3) şi ∅ ; acestea sunt sortate Pentru sv2 = (15, 19, 17, 12, 18) 4 5 6 7 8 a1) poziţionarea lui 15 i=4, j=8 Sensul de parcurgere: ← i=4, j=8 15<18 ⇒ j=j-1=7 i=4, j=7 15>12 ⇒ se efectuează interschimbarea şi se atribuie i=i+1=5 sv2=(12, 19, 17, 15, 18) 4 5 6 7 8 i=5, j=7 Sensul de parcurgere: → i=5, j=7 19>15 ⇒ se efectuează interschimbarea şi se atribuie j=j-1=6 sv2=(12, 15, 17, 19, 18) 4 5 6 7 8 i=5, j=6 Sensul de parcurgere: ← i=5, j=6 15<17 ⇒ j=j-1 i=5, j=5 i=j poziţionare realizată sv2=(12, 15, 17, 19, 18) 4 5 6 7 8 poz = 5 b1)Se obţine secvenţa sv2=(12, 15, 17, 19, 18) 4 5 6 7 8 poz=5; se lucrează mai departe cu secvenţele

82

Algoritmi recursivi. Metodele divide et impera şi backtracking

sv3=(12) sortată sv4=(17, 19, 18) 6 7 8 În acest moment, v=(3, 4, 10, 12, 15, 17, 19, 18) Al doilea nivel de recursie - avem numai secvenţa sv4=(17, 19, 18) 6 7 8 a2) poziţionarea lui 17 i=6, j=8 Sensul de parcurgere: ← i=6, j=8 17<18 ⇒ j=j-1=7 i=6, j=7 19<19 ⇒ j=j-1=6 i=j=6 - poziţionare realizată b2) Se obţine sv4=(17, 19, 18), poz=6, 6 7 8 se lucreauă mai departe cu secvenţele sv5= sortată sv6=(19, 18) 7 8 În acest moment v=(3, 4, 10, 12, 15, 17, 19, 18) Al treilea nivel de recursie - avem numai secvenţa sv6=(19, 18) 7 8 a3) poziţionarea lui 19 i=7, j=8 Sensul de parcurgere: ← i=7, j=8 19>18 ⇒ se interschimbă şi i=i+1=8 sv6=(18,19) i=j=8, poziţionare realizată b3) S-a obţinut secvenţa sv6=(18, 19), poz=8 Următoarele secvenţe sunt sv7=(18), sv8= ∅ , ambele sortate Calculul se încheie, v=(3, 4, 10, 12, 15, 17, 18, 19) este sortat crescător. program quick_sort; uses crt; var x:array[1..100] of integer; n,i:byte; procedure poz(p,u:byte; var k:byte); var i,j:byte; l,di,dj:shortint; {di, dj: pasii de incrementare pentru i si j; ei indica sensul parcurgerii} v:integer;

83

Programarea calculatoarelor – Tehnica programării în limbajul Pascal begin i:=p;j:=u;di:=0;dj:=-1; while i<j do if x[i]>x[j] then begin v:=x[i]; x[i]:=x[j]; x[j]:=v; l:=di;di:=-dj;dj:=-l; i:=i+di;j:=j+dj; end else begin i:=i+di;j:=j+dj; end; k:=i; end; procedure quick(p,u:byte); var i:byte; begin if p>=u then else begin poz(p,u,i); quick(p,i-1); quick(i+1,u); end; end; begin{ program principal} clrscr; write('Dimensiunea vectorului:'); readln(n); for i:=1 to n do read(x[i]); quick(1,n); for i:=1 to n do write(x[i],' '); end.

În programul quick_sort, pentru simularea sensurilor de parcurgere, s-a lucrat cu incremenţi pentru i şi j, desemnaţi prin di, respectiv dj. Pentru sensul de parcurgere “←“ (de la sfârşitul secvenţei spre începutul ei) i rămâne constant (di=0) şi j este decrementat cu o unitate (dj=-1). Pentru celălalt sens, j rămâne constant (dj=0) şi este incrementat i (di=1). Trecerea de la un sens la celălalt, efectuată în momentul unei interschimbări, determină secvenţa de operaţii: l=dj; di=dj; dj=-l; adică di este interschimbat cu – dj. Vectorul de sortat x este variabilă globală procedurii quick. Soluţiile recursive propuse în exemplele precedente se bazează fie pe o metodă de tip reducere, fie pe o metodă de descompunere. În toate cazurile a fost posibilă “sinteza” unei soluţii a problemei date din soluţiile subproblemelor la care problema

84

Algoritmi recursivi. Metodele divide et impera şi backtracking

s-a redus, respectiv în care s-a descompus. De asemenea, pentru fiecare dintre problemele considerate au fost definite subproblemele primitive (condiţiilor terminale) a căror soluţie este “cunoscută” sau dată. Metoda de rezolvare se numeşte divide et impera (dezbină şi stăpâneşte) şi semnifică ideea prin care este realizată construcţia soluţiei.

5.3 Metoda backtracking Pentru rezolvarea anumitor probleme este necesară desfăşurarea unui proces de căutare a soluţiei aflate într-o anumită mulţime, numită spaţiul stărilor. Pentru fiecare element din spaţiul stărilor este definită o mulţime de acţiuni sau alternative. Momentul iniţial în rezolvarea problemei corespunde unei stări, numită iniţială, iar soluţiile corespund drumurilor în spaţiul stărilor, de la cea iniţială până la una finală. Procesul de rezolvare a problemei poate fi imaginat ca o secvenţă de acţiuni care asigură “deplasarea” (prin intermediul unei secvenţe de stări) în spaţiul stărilor din starea iniţială la cea finală. În cazul anumitor probleme se doreşte obţinerea unei singure soluţii, altele solicită determinarea tuturor soluţiilor sau determinarea unei soluţii optime, dintr-un anumit punct de vedere (soluţie optimală). Se presupune că problema constă în a ajunge în vârful unui munte pornind de la baza lui. În general, există mai multe puncte din care se desprind mai multe poteci, dar nu neapărat toate conduc spre vârful muntelui. În acest caz, starea iniţială este baza muntelui, există o singură stare finală şi anume vârful muntelui, spaţiul stărilor incluzând şi toate punctele de ramificare a drumului. O soluţie poate fi apreciată ca optimală din mai multe puncte de vedere: un drum care solicită cel mai mic efort din partea celui care-l urmează; un cel mai scurt drum; un drum care trece printr-un punct preferat etc. Un alt exemplu este un labirint având una sau mai multe ieşiri. Starea iniţială poate fi considerată orice cameră a labirintului, problema revenind la găsirea unui drum din camera respectivă către una dintre ieşiri. Desfăşurarea procesului de căutare a unei stări finale presupune, la fiecare etapă, alegerea opţiunii pentru o alternativă posibilă a stării curente şi detectarea acelor stări “capcană” din care nu mai este posibilă continuarea procesului, sau deja se cunoaşte excluderea atingerii unei stări finale. Detectarea stării “capcană” trebuie să determine revenirea la starea din care s-a ajuns la ea şi selectarea unei noi opţiuni de continuare. În cazul în care nu mai există alternative care să nu fi fost selectate anterior, o astfel de stare devine la rândul ei “capcană” şi pentru ea se aplică acelaşi tratament. Prin soluţie a problemei se înţelege o secvenţă de acţiuni care determină tranziţia din starea iniţială într-o stare finală, fiecare componentă a unui drum soluţie reprezentând o alternativă din mulţimea de variante posibile. Cu alte cuvinte, x1 este alternativa aleasă pentru starea iniţială, x2 este alternativa selectată pentru starea în

85

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

care s-a ajuns pe baza opţiunii x1 ş.a.m.d. După efectuarea acţiunii corespunzătoare alegerii alternativei xn rezultă o stare finală. Forma standard a metodei corespunde unei probleme în care trebuie găsit un drum soluţie x=(x1, x2, ..., xn) cu xi ∈ Si, unde fiecare mulţime Si este finită şi conţine si elemente. În plus, se presupune că fiecare Si este ordonată şi reprezintă mulţimea alternativelor existente la momentul i al căutării. În anumite cazuri interesează obţinerea unei singure soluţii, în altele sunt căutate toate soluţiile problemei sau cele care îndeplinesc un criteriu dat (de exemplu, se maximizează sau minimizează o funcţie f definită pe mulţimea drumurilor soluţie din spaţiul stărilor). Procesul de căutare a unui drum soluţie revine la tentativa de extindere a porţiunii de drum construit, alegând prima alternativă disponibilă pentru starea curentă atinsă. Continuarea drumului poate fi realizată până la atingerea unei stări finale sau până la întâlnirea unei stări capcană (mulţimea vidă de alternative). Dacă este atinsă o stare capcană, atunci este necesară revenirea la starea anterioară şi selectarea următoarei alternative disponibile acestei stări. Dacă nu mai există alternative disponibile, atunci se iniţiază o nouă revenire ş.a.m.d. În cazul în care există cel puţin încă o alternativă disponibilă, atunci se reia procesul de extindere a drumului rezultat. În condiţiile în care revenirea poate conduce la atingerea stării iniţiale şi pentru ea nu mai există alternative disponibile, se consideră că problema nu are soluţie. Pentru implementarea căutării este necesară reţinerea alternativei selectate pentru fiecare stare atinsă până la cea curentă, astfel încât, în cazul unei reveniri să fie posibilă alegerea alternativei următoare. Cu alte cuvinte, procesul de căutare revine la tentativa de extindere a drumului curent (pasul de continuare), cu eventuala revenire în cazul atingerii unei stări capcană (pasul de revenire - back), memorând alternativele selectate pentru fiecare stare intermediară atinsă (track). De aici îşi are geneza numele metodei backtracking. Pentru determinarea unei singure soluţii, descrierea pe paşi a metodei este: • starea iniţială a problemei este prima alternativă posibilă pentru starea curentă ; fie aceasta x1 ∈ S1; • dacă starea curentă rezultată prin alternativa x1 este finală, atunci x=(x1) este soluţie; stop; • altfel, este selectată prima alternativă din mulţimea de acţiuni posibile pentru starea curentă; fie aceasta x2 ∈ S2; • dacă secvenţa de alternative care a condus la starea curentă este x=(x1, x2,..., xk), atunci: • dacă starea curentă este finală, soluţia este x=(x1, x2,..., xk); stop; • altfel • B1: dacă pentru starea curentă există alternative disponibile, atunci se alege prima dintre ele şi se continuă;

86

Algoritmi recursivi. Metodele divide et impera şi backtracking

• B2: altfel, se revine la starea anterioară celei curente, soluţia parţial construită devine x=(x1, x2,..., xk-1) şi se efectuează B1. • dacă, în urma unui pas de revenire, s-a ajuns la starea iniţială şi nu mai sunt alternative disponibile, atunci problema nu are soluţie; stop. În cazul în care trebuie determinate toate soluţiile problemei, căutarea continuă după determinarea fiecărei soluţii prin efectuarea de reveniri succesive. Terminarea căutării este decisă în momentul în care s-a revenit la starea iniţială şi nu mai există alternative disponibile. Dacă se doreşte obţinerea numai a soluţiilor care optimizează o funcţie criteriu f, atunci metoda se aplică pentru determinarea tuturor soluţiilor problemei, fiecare nouă soluţie rezultată fiind comparată cu “cea mai bună” soluţie determinată anterior. Pentru aceasta este necesară reţinerea “celei mai bune” soluţii calculate la fiecare moment. Forma generală a metodei backtracking este implementată de procedura back. procedure back(k:byte); begin if k=n+1 then final else begin x[k]:=init(k); while succ(k) do if continuare(k) then back(k+1); end; end;

în care: − final este o procedură care descrie prelucrarea dorită pentru o soluţie determinată (se afişează rezultatul, se testează o funcţie criteriu pentru soluţia obţinută samd); − init(k) efectuează iniţializarea lui xk cu o valoare prin care se indică faptul că, până la acel moment, nu a fost selectată nici o alternativă pentru poziţia k; − succ(k) este o funcţie booleană care calculează true, dacă şi numai dacă există succesor pentru xk în Sk; − continuare(k) este o funcţie booleană pentru testarea condiţiilor de continuare; calculează true dacă şi numai dacă este posibilă extinderea drumului curent. În continuare sunt prezentate câteva probleme rezolvate prin metoda backtracking. 1. Să se genereze toate permutările mulţimii {1, 2,..., n}. În acest caz, S1 = S2 = ... = Sn = {1, 2,..., n}. Alternativele posibile pentru starea iniţială corespund alegerilor pentru prima poziţie dintr-un vector soluţie. Pentru fiecare k, 1 ≤ k ≤ n − 1 , dacă x=(x1, x2,..., xk) este drumul calculat până la

87

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

momentul k, atunci xi ≠ x j , ∀1 ≤ i ≠ j ≤ k şi alternativele posibile pentru starea curentă sunt elementele xk+1 din mulţimea {1, 2,..., n}care îndeplinesc cerinţa xi ≠ x k +1 , ∀1 ≤ i ≤ k . De exemplu, pentru n=3, soluţiile problemei sunt: x1=(1,2,3), x2=(1,3,2), x3=(2,1,3), x4=(2,3,1), x5=(3,1,2), x6=(3,2,1). Funcţia init(k) realizează iniţializarea elementului x[k] cu valoarea 0, pentru a marca faptul că, până la momentul curent, nu a fost selectată nici o alternativă pentru x[k]. Funcţia succ(k) calculează true dacă elementul x[k] are succesor în mulţimea {1, 2,..., n}, caz în care acesta este determinat prin incrementare. Altfel, funcţia calculează false. Funcţia continuare(k) returnează true dacă şi numai dacă secvenţa (x1, x2,..., xk) calculată până la momentul curent este corectă, conform regulilor descrise anterior. Conform schemei generale, programul Pascal este: program permutare; uses crt; type tip_elem=0..7; var x:array[1..7] of tip_elem; n:byte; function init:byte; begin init:=0; end; function succ(k:byte):boolean; begin succ:=x[k]x[k]) do inc(i); continuare:=i=k; {atribuire de valoare logica} end; procedure final; var i:byte; begin for i:=1 to n do write(x[i],' '); readln; end; procedure back(k:byte); begin if k=n+1 then final else begin x[k]:=init; while succ(k) do if continuare(k) then back(k+1);

88

Algoritmi recursivi. Metodele divide et impera şi backtracking end; end; begin clrscr; write('Numarul de elemente ale permutarii: '); readln(n); back(1); end.

În cazul acestei probleme este posibilă operarea unor simplificări în scrierea codului, pe baza observaţiilor: - funcţia init(k) nu depinde de valoarea lui k şi returnează întotdeauna valoarea 0; - funcţia succ(k) nu depinde de valoarea parametrului k şi realizează întotdeauna o incrementare (S1 = S2 = ... =Sn = {1, 2,..., n}). Procedura back(k) poate fi descrisă fără a utiliza funcţiile init şi succ, astfel: procedure back(k:byte); var i:byte; begin if k=n+1 then final else for i:=1 to n do begin x[k]:=i; if continuare(k) then back(k+1); end; end;

Înlocuirea în procedura back a structurii repetitive while cu ciclul for este posibilă datorită faptului că funcţia succ realiza incrementarea valorii elementului x[k]. Varianta de program Pascal rezultată în urma acestor simplificări este: program permutare_1; uses crt; type tip_elem=0..7; var x:array[1..7] of tip_elem; n:byte; procedure final; var i:byte; begin for i:=1 to n do write(x[i],' '); readln; end; function continuare(k:byte):boolean; var i:byte; begin i:=1; while(ix[k]) do inc(i); continuare:=i=k; end; procedure back(k:byte);

89

Programarea calculatoarelor – Tehnica programării în limbajul Pascal var i:byte; begin if k=n+1 then final else for i:=1 to n do begin x[k]:=i; if continuare(k) then back(k+1); end; end; begin clrscr; write('Numarul de elemente ale permutarii: '); readln(n); back(1); end.

2. Se presupune că se dispune de n tipuri de bancnote, n ≤ 20, cu valori diferite; din fiecare tip se dispune de un număr cunoscut de bancnote. Considerându-se dată o sumă de bani s, să se determine o modalitatea de plată a sa utilizându-se un număr minim de bancnote. Valorile şi numărul de bancnote disponibile din fiecare tip sunt memorate într-o tabelă y cu 2 linii şi n coloane, astfel: pentru fiecare 1 ≤ i ≤ n , y[1,i] reprezintă valoarea unei bancnote de tipul i şi y[2,i] este numărul de bancnote disponibile din tipul i. Vectorul xb memorează modalitatea optimă de plată a sumei s, adică xb[i] reprezintă numărul de bancnote alese din tipul i într-o descompunere optimă; vectorul x memorează descompunerea curentă. Numărul minim de bancnote utilizate în plata sumei s este nrb. Metoda utilizată în rezolvarea problemei este backtracking, urmărindu-se minimizarea funcţiei f ( x ) =

n

∑ x[i] ,

n

în condiţiile în care

i =1

∑ x[i]y[1, i] = s

şi

i =1

0 ≤ x[i] ≤ y[2, i], i = 1,..., n . Sunt generate toate descompunerile posibile ale sumei s funcţie de bancnotele disponibile şi, la fiecare moment în care este determinată o astfel de descompunere, aceasta este comparată cu precedenta. În vectorul xb este memorată o cea mai bună descompunere pe baza criteriului f, după fiecare astfel de comparaţie. program bancnote; uses crt; var x,xb:array[1..100] of longint; y:array[1..2,1..20]of longint; n,i,nrb:word; s:longint; function init(k:word):longint; begin init:=-1; end; function urm(k:word):boolean; begin

90

Algoritmi recursivi. Metodele divide et impera şi backtracking urm:=x[k]
91

REPREZENTAREA VIZUALĂ A DATELOR

Forma şi formatul afişării datelor pe monitor (ca ecou al introducerii de la tastatură sau ca rezultat al scrierii) precum şi modul în care se desfăşoară conversaţia în timpul acestui proces constituie interfaţa programului cu utilizatorul. Realizarea unei interfeţe eficiente şi atractive trebuie să constituie pentru programator un obiectiv la fel de important ca şi cel al atingerii performanţelor ridicate de eficienţă a programului însuşi.

6.1 Resursele şi modurile de lucru video ale unui microcalculator În procesul afişării video a datelor sunt implicate componente hardware şi software, care interacţionează pentru crearea imaginii pe ecranul monitorului (figura 6.1).

Placa grafică Interfaţa video (Logică de comandă)

Monitor

Software Memoria ecran Fig. 6.1 Componente implicate în afişarea video a datelor • Memoria ecran, de tipul RAM, are rolul de a stoca, sub formă binară, imaginea care se afişează pe ecran. Ea poate avea diferite capacităţi - uzual, până la 1

92

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

MB − şi este împărţită în pagini de o anumită mărime, în care se pot memora concomitent una sau mai multe imagini de ecran. În plus, se pot memora definiţiile binare ale unor seturi de caractere pentru afişarea textelor cu modele de caractere (fonturi) ale utilizatorului, altele decât cele definite de generatorul de caractere al interfeţei video. • Interfaţa video (adaptorul video) are rolul de a comanda şi controla monitorul pentru crearea imaginii pe tub catodic. Ea comunică cu UC a microcalculatorului şi cu memoria ecran prin magistrale de date şi de adrese. UC şi interfaţa pot utiliza simultan memoria ecran (prin porturi diferite), astfel încât microprocesorul poate înscrie date în memoria ecran în timp ce interfaţa citeşte memoria pentru a realiza afişarea (figura 6.2).

Microproceso r

Magistrala de date Magistrala de adrese

Memoria ecran

Interfaţă video Sincronizare Controler video

Generator de caractere

Registru de deplasare

Generator de semnale

Semnale de comandă Monitor Fig. 6.2 Modul de interacţiune a interfeţei video Controlerul video (logica de comandă) al interfeţei citeşte periodic, cu o anumită frecvenţă, memoria de ecran şi formează semnalele de comandă pentru monitor în vederea creării şi menţinerii imaginii video. Dacă frecvenţa de refacere a imaginii pe ecran (frecvenţa de reîmprospătare) este de peste 50 Hz (de obicei 50-70 Hz), atunci, datorită inerţiei ochiului, imaginea apare ca fiind continuă şi fără pâlpâiri. Generatorul de caractere este dispozitivul interfeţei care produce imaginile (matricele) punctiforme ale caracterelor ASCII de afişat. Prin comanda dată de controler şi utilizând caracterele ASCII citite din memoria ecran, generatorul de caractere produce, la ieşire, matricele discrete care definesc caracterele. Pe baza lor şi a atributelor de afişare asociate, aduse de controler din memoria de ecran, generatorul de semnale produce semnalele seriale care comandă monitorul. Serializarea în producerea semnalelor este asigurată prin intermediul unui registru de deplasare care furnizează, pe rând, câte un element de imagine.

93

Reprezentarea vizuală a datelor • Monitorul utilizează un tub catodic monocrom sau color şi creează imaginea pe ecran, de sus în jos. Fascicolul de electroni “aprinde”, pe rânduri sau linii, puncte elementare din suprafaţa fluorescentă a tubului catodic, în conformitate cu semnalele discrete de comandă trimise de sistemul de baleere a ecranului. Punctele elementare de imagine sunt denumite pixeli (picture elements) şi apar pe monitor organizate matriceal, definind spaţiul fizic sau spaţiul ecran (figura 6.3). În acest spaţiu, pixelii pot fi adresaţi printr-o pereche de numere naturale (x, y), unde x este numărul coloanei şi y este numărul liniei. De remarcat faptul că, în acest spaţiu, abscisa creşte normal, de la stânga spre dreapta, dar ordonata creşte de sus în jos.

(0,0)

x

•••• •••• ••••

•••• •••• •••• • (x,y) •••• ••••

•••• ••••

y

(xmax,ymax) Fig. 6.3 Spaţiul ecran

Numărul total de pixeli care pot fi trataţi independent constituie o caracteristică a monitorului, denumită rezoluţia ecranului. Rezoluţia poate fi joasă (cca.320 x 200 pixeli), medie (cca.640 x 200 pixeli) şi înaltă (peste 640 x 350 pixeli). Interfeţele video sunt proiectate astfel încât pot lucra cu monitoare de diferite rezoluţii, pentru un monitor simulând toate rezoluţiile inferioare celei date. Imaginea este cu atât mai clară cu cât rezoluţia utilizată în realizarea ei este mai mare. De regulă, în construcţia microcal culatoarelor, interfaţa video şi memoria de ecran sunt realizate pe o placă unică, numită placă grafică. “Puterea” plăcii grafice este cea care dă, în principal, puterea de afişare a datelor pe ecran. În prezent, microcalculatoarele sunt echipate cu plăci grafice de mare putere, de tipul SVGA (Super Video Graphic Array), realizate după standardul firmei IBM pentru utilizatori profesionali. Ele sunt compatibile cu plăci realizate anterior (MDA, CGA şi EGA), al căror mod de lucru îl pot realiza, oferind în plus o rezoluţie înaltă şi o paletă bogată de culori şi nuanţe de gri (pentru monitoare monocrome). • Partea de software implicată în afişarea video are rolul de a comanda interfaţa video în realizarea diferitelor operaţii necesare afişării: alegerea modului de afişare - text sau grafic - definirea poziţiei spaţiului ecran pentru afişare, definirea atributelor de afişare etc. Ea este constituită din rutine BIOS multifuncţionale (cu mai multe servicii) care se apelează prin intermediul tehnicii întreruperilor software. Tipică este INT 10h, care oferă o serie de servicii video standard. În program, înainte de cererea întreruperii, codul serviciului dorit trebuie încărcat într-un registru special

94

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

(AH), iar alţi parametri, dacă sunt necesari, se încarcă în alte registre (AL, BH, BL, DH, DL).

6.2 Moduri video Din punctul de vedere al constituirii imaginii, interfaţa video poate lucra în două moduri: text şi grafic. • În modul text, ecranul este văzut ca o matrice de linii şi coloane de text. Locul de afişare a unui caracter în spaţiul ecran este dat de o pereche (x,y) de numere naturale, cu x=numărul coloanei şi y=numărul liniei (figura 6.4.a).

cmax

012 0 1

x

(nr. coloană) 8 #•#####• #•#####• #••####• #••####• #•#•###• #•#••##• #•##•##• 14 #•##••#• #•###•#• #•####•• #•####•• #•#####• #•#####• ########

caracter lmax

y

(nr. linie) ecran 25*80

lmax =24 cmax =79 lmax =24

ecran 25*40 cmax =39 a) Spaţiul

b) Matricea de caractere

Fig. 6.4 Ecranul în modul text Deoarece spaţiul text rezultă prin gruparea corespunzătoare a liniilor şi coloanelor de pixeli, afişarea caracterelor se face, în mod natural, prin matrice de caractere (figura 6.4.b). O astfel de matrice, de regulă 8x14 sau 8x8, defineşte forma caracterului prin pixelii aprinşi, iar fondul prin pixelii stinşi. În fondul caracterului se cuprind pixeli de separare a caracterelor pe coloane şi linii. Caracterele realizate astfel se numesc caractere matriceale sau rastru. De regulă, în memoria ROM a calculatorului se definesc patru seturi de caractere, având forme (fonturi) tipice (scriere standard, caractere italice etc). La pornirea calculatorului sau la cererea

95

Reprezentarea vizuală a datelor

programatorului, ele sunt încărcate în memoria de ecran pentru a fi accesibile generatorului de caractere. Un anumit set, implicit sau cerut de utilizator (via INT 10h), va deveni set curent pentru generator. Există posibilitatea ca programatorul să-şi definească în memoria calculatorului propriile seturi de caractere (cel mult patru odată), pe care să le încarce în memoria ecran şi din care, apoi, să definească setul curent (ambele sunt servicii INT 10h). Datorită adresabilităţii spaţiului text, există posibilitatea de a comanda (INT 10h) poziţia de afişare a unui caracter. În acest scop, interfaţa păstrează în registre speciale coordonatele locului de afişare, iar pe ecran acesta este marcat prin afişarea unui caracter special, denumit cursor (de regulă, un dreptunghi umplut sau luminos). Atunci când se afişează un caracter în poziţia de la cursor (poziţia curentă), acesta se suprascrie peste cursor, iar cursorul este mutat automat cu o poziţie spre dreapta şi în jos, la începutul noului rând, dacă este cazul. Prin servicii ale rutinei INT 10h se poate defini o formă proprie a cursorului în modul text şi se poate afla locul acestuia pe ecran. Fiecărui caracter de afişat i se asociază un caracater de atribute (figura 6.5.a).

a) caracter de atribute 76543210

byte

b) registru de paletă 0 1 2 ... 15

culoare negru albastru cod

0

1

... ...

alb 53

Culoare de Culoare de fond caracter Clipire (1=da, 0=nu)

Intensitatea culorii (1=da, 0=nu) Fig. 6.5 Definirea atributelor de afişare

Caracterul de atribute, prin valorile biţilor săi, defineşte culoarea de fond a caracterului (background color), culoarea de caracter (foreground color) şi faptul că imaginea caracterului se va “stinge intermitent” (clipire - blinking). Se observă că se pot defini cel mult 8 culori pentru fond şi 16 culori de caracter, dacă se consideră că afişarea normală şi intensă definesc două culori diferite. Dacă ecranul este monocrom, atunci se pot combina culorile alb şi negru. Pentru variabilitate în exprimarea culorilor şi pentru asigurarea independenţei faţă de convenţiile de notare a lor la diverse plăci grafice, s-a adoptat ideea specificării indirecte a culorilor. Interfaţa posedă un registru, de regulă de 16 octeţi, denumit registru de paletă, prin care se specifică culorile care pot fi utilizate la un moment dat (figura 6.5.b). Fiecare poziţie din registrul de paletă defineşte, prin cod, o culoare şi, în ansamblu, constituie paleta curentă de culori.

96

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Astfel, în caracterele de atribute se specifică numărul intrării din paleta curentă (index), de unde trebuie extras codul culorii dorite. Pe baza acestei idei, în orice moment, poate fi modificat conţinutul registrului de paletă (serviciu INT 10h), ceea ce conduce, în mod automat, la modificarea culorilor pentru întregul text afişat pe ecran. La pornirea calculatorului, registrul de paletă este încărcat cu o paletă iniţială (implicită), definită în memoria ROM. Programatorul poate schimba, total sau parţial, culorile în registrul de paletă, depinzând de tipul plăcii grafice şi aceasta este unica operaţie pentru care trebuie cunoscute codurile culorilor la placa respectivă. Trebuie remarcat că unele monitoare nu pot afişa toate culorile, deşi interfaţa cu care sunt cuplate acceptă specificarea oricărui atribut. Definirea caracterului de atribute se face prin serviciul INT 10h, care realizează scrierea caracterului text ca parametru într-un registru (registrul BL). Scrierea unui caracter poate fi însoţită de operaţia SAU EXCLUSIV (suma modulo-2, notată ⊕) între culoarea caracterului afişat într-o anumită poziţie şi culoarea noului caracter, destinat a fi afişat în aceeaşi poziţie. Operaţia poate fi utilizată pentru scrierea şi ştergerea caracterelor (ştergerea selectivă). De exemplu, pentru ştergere, dacă culoarea este 0101 şi se aplică o suprascriere cu aceeaşi culoare, atunci, cum 0101⊕0101=0000, se obţine un cod care desemnează culoarea fondului curent, adică, de fapt, “dispariţia” caracterului de pe ecran. Având în vedere că necesarul de memorie ecran este în jur de 4KB 25*80, depinzând de rezoluţia în modulul text, în memoria video se pot focaliza una sau mai multe pagini video (ecrane). Se creează posibilitatea constituirii textului într-o anumită pagină, denumită pagină video, selectată din mulţimea paginilor acceptate de interfaţa respectivă prin serviciul INT 10h. Orice poziţie şi deplasare a cursorului pe ecran îşi are echivalentul într-o poziţie şi o deplasare corespunzătoare în pagina video selectată. Din mulţimea paginilor definite, programatorul poate selecta una, ca pagină de afişare pe ecran, denumită pagină activă. Astfel creşte viteza de lucru prin suprapunerea operaţiei de afişare cu operaţia de pregătire a imaginii, într-o nouă pagină video. Interogarea, prin INT 10h, în legătură cu atributele unui anumit caracter al imaginii este, de fapt, o citire din pagina video a memoriei ecran, eventual pagina activă. Modul text permite definirea, în pagina activă, a ferestrelor de text. O fereastră text este o zonă de afişare, formată din una sau mai multe linii şi coloane consecutive. Ferestrele text se definesc şi se selectează prin INT 10h. Definirea se realizează prin precizarea coordonatelor din spaţiul text ale colţului stânga sus şi respectiv ale colţului dreapta jos. În fereastră se poate realiza o operaţie de defilare în sus (jos) care deplasează textul rândurilor cu un număr de poziţii în sus (jos) şi umple rândurile eliberate, la partea de jos (sus), cu caracterul blank (ştergere). Se pot scrie rânduri noi de text în rândurile eliberate, eventual în toată fereastra activă. Rândurile de text care ies din fereastră, prin defilare, se pierd. La limită şi în mod implicit, fereastra text este întreaga pagină activă. În modul grafic, imaginea se constituie prin aprinderea mulţimilor de pixeli (desen prin puncte - pixel mode). Imaginea se creează în memoria ecran, în pagini, dacă placa grafică permite mai multe pagini şi se afişează pe ecran, pagină cu pagină.

97

Reprezentarea vizuală a datelor

O pagină video utilizează un bit pentru a defini starea fiecărui pixel al ecranului (1=aprins, 0=stins) şi un număr de biţi suplimentari pentru a defini culoarea acestora. Deoarece culorile se definesc pe baza registului de paletă, dacă se utilizează concomitent 16 culori, sunt necesari 4 biţi suplimentari. Rezultă că o pagină video trebuie să aibă cel puţin 4∗rezoluţie biţi. De exemplu, pentru o placă EGA, cu rezoluţie înaltă, sunt necesari 4∗640∗350=896000 biţi≈110 Kb şi deci, într-o memorie ecran de 256 Kb, pot fi create două pagini video. Modul grafic utilizează posibilitatea de adresare a pixelilor într-o pagină video şi un cursor grafic invizibil. Cursorul grafic are coordonatele ultimului pixel adresat în pagina video selectată şi poate fi deplasat în orice punct al acestui spaţiu. Se menţin, de asemenea, facilităţile de interogare asupra poziţiei cursorului şi asupra culorii pixelului curent. Ca şi la modul text, la definirea unui pixel există posibilitatea realizării operaţiei SAU EXCLUSIV între biţii de culoare ai unui pixel dintr-o pagină şi valorile noi ale acestora. În acest fel, pixelii pot fi scrişi sau şterşi. Toate aceste operaţii se realizează ca servicii ale rutinei INT 10h. În modul grafic, se selectează un anumit submod acceptat de placa grafică, submodurile diferind prin rezoluţie, număr de culori şi număr de pagini de ecran pe care le acceptă.

6.3 Facilităţi de scriere a textelor prin unitatea CRT În Pascal, operaţiile de afişare pe monitor, în modul text, au fost implementate în două unităţi de program: SYSTEM şi CRT. Unitul SYSTEM defineşte operaţia de afişare pe monitor în mod standard, adică cu atribute de scriere implicite şi începând din poziţia curentă a cursorului. Unitul CRT (anexa 3) defineşte variabile, funcţii şi proceduri prin care programatorul poate defini şi controla contextul de scriere (ferestre, culori, poziţie de scriere etc.) • Alegerea modului text. Programatorul are posibilitatea de a alege un anumit mod de scriere, caracterizat, în principal, prin rezoluţia de afişare şi tipul de monitor: monocrom sau color. Sunt acceptate modurile prezentate în anexa 3. Pentru alegerea modului se apelează procedura: TextMode (Mode); în care parametrul Mode, de tip Word, defineşte modul dorit, prin numărul asociat. Procedura salvează vechiul mod în variabila predefinită LastMode, de unde, prin aceeaşi procedură, poate fi restaurat. Procedura setează culoarea de fond zero (culoarea neagră). Dacă nu se apelează TextMode, atunci, implicit, se consideră modul de mare rezoluţie al plăcii grafice.

98

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Exemplu: 6.1. . . . User CRT; . . . TextMode(C080); . . . TextMode(LastMode); . . . TextMode(BW40)

• Stabilirea culorii de fond şi de scriere se face prin referire la paleta implicită. Intrările în paletă, desemnate prin constante întregi sau constante simbolice, sunt asociate cu diferitele culori (anexa 3). Definirea culorii fondului se face prin apelul procedurii: TextBackGround(culoare); în care parametrul culoare este de tipul Byte şi poate avea valorile 0-7. Procedura tratează biţii 4-6 ai unei variabile predefinite TextAtrr de tip Byte, prin care se modifică corespunzător octetul de atribute din memoria video. Culoarea de scriere se defineşte prin apelul procedurii: TextColor (culoare); în care parametrul culoare poate să aibă o valoare între 0 şi 15 (anexa 2), eventual mărită cu 128. Procedura modifică biţii 0-3 şi 7 ai variabilei TextAtrr, scriind în bitul 7, pentru definirea clipirii, 1 sau 0, după cum a fost adunată sau nu constanta 128 (constanta simbolică corespunzătoare este Blink).

Exemplu: 6.2. . . . TextColor (Red+128); . . . TextColor(Green+Blink); . . . Culoare:=6 . . . TextColor(Culoare);

Programatorul poate controla intensitatea culorii, prin modificarea bitului 3 din variabila TextAtrr (şters de către TextColor) prin procedurile fără parametri: HighVideo - intensitate mărită (bitul 3 are valoare 1); LowVideo - intensitate mică (bitul 3 are valoare 0); NormVideo - intensitate implicită. Execuţia procedurilor pentru stabilirea culorii de fond şi de scriere afectează numai fereastra curentă. Atributele rămân valabile până la o nouă definire sau până la terminarea execuţiei programului. • Definirea unei ferestre de scriere. Scrierea textului pe ecran se poate realiza într-o anumită zonă a acestuia, denumită fereastră curentă de scriere (text

99

Reprezentarea vizuală a datelor

window). O fereastră se defineşte ca un dreptunghi, prin coordonatele colţurilor stânga-sus, dreapta-jos. Ecranul întreg este o fereastră de coordonate (1, 1, 80, 25) sau (1, 1, 40, 25) etc., depinzând de modul text. Declararea ferestrei se face prin procedura: Window(x1, y1, x2, y2); în care parametrii (de tipul Byte) definesc, în ordine, cele două colţuri: (x1, y1) colţul stânga-sus, (x2, y2) - colţul dreapta-jos (în exprimare matriceală, abscisele x1,x2 reprezintă numerele coloanelor, iar ordonatele y1, y2 sunt numerele de linii). Exemplu: 6.3. Window (10, 5, 70, 20); defineşte o fereastră care se întinde între liniile 5-20 şi coloanele 10-70. Unitatea CRT defineşte variabilele WindMin, WindMax, de tipul Word, care conţin coordonatele colţurilor ferestrei curente. Octetul cel mai semnificativ conţine numărul liniei (y), iar cel mai puţin semnificativ conţine numărul coloanei (x). Coordonatele ferestrei curente pot fi scrise ca: (Lo(WindMin), Hi(WindMin)) - colţul stânga-sus (Lo(WindMax), Hi(WindMax)) - colţul dreapta-jos Dacă nu se apelează procedura Window, fereastra implicită este întregul ecran, variabilele WindMin şi WindMax fiind iniţializate corespunzător modului text selectat sau implicit. Fereastra selectată rămâne activă până la un nou apel al procedurii Window, care poate defini o nouă fereastră curentă, eventual una utilizată anterior. • Poziţionarea cursorului. În fereastra curentă, programatorul poate să gestioneze poziţia cursorului după necesităţile programului, impunând locul pe ecran începând cu care se va realiza scrierea următorului text. Poziţia curentă a cursorului poate fi aflată prin apelul funcţiilor, fără parametri, cu rezultat de tipul Byte: WhereX (pentru abscisă) şi WhereY (pentru ordonată). Mutarea cursorului într-o anumită poziţie a fereastrei curente poate fi comandată prin apelul procedurii: GotoXY(X, Y); Exemplu: 6.4. . . . Window(20, 3, 100, 15); TextBackGround(Red); TextColor(Blue); ClrScr; {sterge fereastra} Writeln (’Text 1’); {Se scrie pe linia 1, coloana 1 a ferestrei (coordonatele absolute: linia 3, coloana 20)} GotoXY(5, 6); Write (’Text 2’); {Se scrie pe linia 6, coloana 5 a ferestrei (coordonatele absolute: linia 9, coloana 25)} X:=WhereX; Y:=WhereY; Write; Writeln (’X=’, X, ’Y=’, Y); {Se scrie X=11, Y=6}

Dacă programatorul nu gestionează poziţia cursorului, atunci acesta se deplasează, implicit, de sus în jos şi de la stânga la dreapta, pe măsură ce se scrie

100

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

textul în fereastră. Când cursorul este la sfârşitul ultimului rând al ferestrei, se produce defilarea textului în fereastra curentă. • Ştergerea şi inserarea în fereastra curentă se pot realiza utilizând următoarele proceduri fără parametri: ClrScr - şterge ferestra curentă şi poziţionează cursorul pe prima linie şi coloană a acesteia; ClrEol - şterge linia curentă, de la cursor până la sfârşitul ei, fără a muta cursorul; DelLine - şterge rândul pe care se află cursorul şi mută, în sus cu un rând, textul de sub linia ştearsă; nu este afectată poziţia cursorului; InsLine - deplasează în jos, cu un rând, textul, începând cu cel pe care este cursorul şi inserează o linie goală, în linia din fereastră pe care este cursorul. Se “pierde” ultimul rând din fereastră. • Citirea caracterelor fără ecou. Prin intermediul a două funcţii, fără parametri, programatorul poate citi fără ecou caracterul produs de ultima tastă apăsată sau poate verifica dacă a fost apăsată o tastă. Se utilizează funcţiile: ReadKey - funcţie de tipul CHAR care reîntoarce caracterul existent în buffer-ul tastaturii; dacă nu există un astfel de caracter, atunci aşteaptă până la tastarea unui caracter; KeyPressed - funcţie de tipul BOOLEAN care reîntoarce valoarea True dacă a fost apăsată o tastă şi False altminteri. Funcţia ReadKey întoarce codul caracterului rezultat prin apăsarea tastei, dacă aceasta produce un singur cod sau primul cod (valoare zero binar), dacă tasta apăsată produce două coduri. În acest ultim caz, se obţine caracterul complet dacă se fac două apeluri succesive ale funcţiei Readkey. În general, tastele F1-F12, tastele de deplasare cursor (săgeţi), Home, Ins, Del, PgUp, PgDwn când sunt apăsate singure sau simultan cu CTRL, Shift ori ALT, produc două coduri. Pot, de asemenea, produce două coduri tastele de litere sau cifre, atunci când sunt apăsate simultan cu tasta ALT. În legătură cu funcţia KeyPressed, trebuie menţionat că răspunsul este furnizat potrivit stării buffer-ului tastaturii de la acel moment (funcţia nu aşteaptă apăsarea pe o tastă). Dacă KeyPressed este TRUE, atunci ReadKey returnează imediat caracterul.

101

Reprezentarea vizuală a datelor

• Pornirea şi oprirea difuzorului. Programul poate provoca producerea sunetelor de o anumită frecvenţă pe o durată controlabilă. În acest scop sunt prevăzute procedurile: Sound(Hz) - pornirea difuzorului care emite un sunet continuu de frecvenţă dată de parametrul Hz, în Hertz; NoSound - oprirea difuzorului; Delay(Ms) - realizarea unei întârzieri de Ms milisecunde până la execuţia următoarei instrucţiuni. Controlând corespunzător frecvenţa Hz şi intervalul de emisie Ms al difuzorului se poate realiza o anumită linie melodică. Pentru cei care doresc să se iniţieze în tehnica compunerii muzicii cu ajutorul calculatorului, sunt prezentate în continuare câteva elemente de bază. În tabelul 6.1 sunt prezentate frecvenţele notelor, în patru octave consecutive (în total sunt 7 octave numerotate 0÷6). Se poate constata că frecvenţa unei note într-o octavă se poate estima prin dublarea frecvenţei acesteia din octava imediat inferioară. Similar, se poate deduce frecvenţa notelor dintr-o octavă inferioară prin înjumătăţirea frecvenţelor notelor din octava imediat superioară. Tonul unei note poate fi alterat (mărit sau micşorat) cu o jumătate de ton în octava respectivă (note cu diez şi bemol). Aceasta înseamnă că frecvenţa unei note oarecare i cu diez se calculează cu relaţia: notai:=0.5(notai+1+notai); notai=1,2,4,5,6.

Notele pot avea diferite durate: întreagă, doime, pătrime, optime etc. Dacă se stabileşte durata notei întregi ca durată iniţială, atunci duratele celorlalte note se calculează ca o fracţie corespunzătoare din aceasta (1/2, 1/4, 1/8 etc.). Tabelul 6.1 - Frecvenţele notelor muzicale în diverse octave Octava 1

Octava 2

Octava 3 (mijlocie)

Octava 4

Nota

Frecvenţa

Nota

Frecvenţa

Nota

Frecvenţa

Nota

Frecvenţa

Do

130.81

Do

261.63

Do

523.25

Do

1046.5

Re

146.83

Re

293.66

Re

587.33

Re

1174.7

Mi

164.81

Mi

329.63

Mi

659.26

Mi

1318.5

Fa

174.61

Fa

349.23

Fa

698.46

Fa

1396.9

Sol

186.00

Sol

392.00

Sol

783.99

Sol

1568.0

La

220.00

La

440.00

La

880.00

La

1760.0

Si

246.94

Si

493.88

Si

987.77

Si

1976.5

102

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Se ştie că durata unitară este determinată de tempo-ul în care trebuie “cântate” notele. În tabelul 6.2, sunt redate câteva tempo-uri, exprimate în pătrimi/minut. Din tabel rezultă duratele diferitelor note, în minute, pentru un tempo, T, dat: D1=4/T; D8=1/2T; D2=2/T; D16=1/4T; D4=1/T; D32=1/8T etc. Durata unei note poate fi modificată prin punct. Astfel, durata reală este 3/2 din durata notei fără punct. Pentru a obţine stilul legato şi stacatto, durata pe care se cântă o notă se modifică astfel: normal: 7/8 din durată; 1/8 pauză; legato: 1/1 din durată; stacatto: 3/4 din durată; 1/4 pauză. Trebuie observat că, datorită dependenţei duratei notelor de ceasul intern, pentru unele tempo-uri nu se obţin rezultate satisfăcătoare. De aceea, este necesar ca durata notelor şi tempo-urile să se stabilească prin experimentări. Tabelul 6.2 Duratele notelor în diverse tempo-uri Tempo foarte încet

Denumire Largo Larghetto Adagio

Pătrimi/minut 40-60 60-66 66-76

încet

Andante

76-108

mediu

Moderato

108-120

repede

Alegro Presto

120-168 168-208

6.4 Implementarea modului grafic în Pascal Subprogramele care implementează modul grafic se bazează pe caracteristicile plăcilor existente şi se împart în: primitive grafice şi subprograme ajutătoare. Ele sunt declarate în unitatea Graph şi sunt memorate în următoarele fişiere: • GRAPH.TPU - care conţine codul obiect al subprogramelor definite în unitatea Graph; • *.BGI - câte un fişier pentru fiecare tip de placă grafică, care conţine codul obiect al rutinelor de tratare a întreruperii pentru operaţii grafice (driver de placă), ca soft specific pentru comanda interfeţelor grafice;

103

Reprezentarea vizuală a datelor

• *.CHR

- fişiere care conţin seturi de caractere suplimentare cu diferite stiluri (fonturi). Unitatea Graph defineşte, de asemenea, o serie de constante, variabile şi structuri de date care să-i uşureze programatorului apelul acestor subprograme (anexa 4). Pentru utilizarea subprogramelor de grafică, programatorul trebuie să procedeze astfel: a) Să declare faptul că utilizează unitatea: uses Graph; b) Să selecteze modul (submodul) grafic dorit, prin utilizarea procedurii: InitGraph (GraphDriver, GraphMode, PathToDriver) Primii doi parametri sunt variabile de tipul INTEGER, iar ultimul este o constantă sau variabilă de tipul STRING. Parametrii au următoarea semnificaţie: • GraphDriver precizează tipul plăcii grafice considerate. Variabila poate lua una din valorile întregi 0-10 sau constantele predefinite echivalente. Constanta Detect (valoare 0) cere subprogramului să identifice tipul de placă a calculatorului. • GraphMode selectează unul din submodurile acceptate de placa grafică. Valoarea parametrului nu este luată în considerare dacă GraphDriver a fost Detect, caz în care se selectează automat submodul cu cea mai mare rezoluţie. • PathToDriver defineşte calea spre directorul de rezidenţă a driver-ului de placă (fişier de tip BGI), dacă acesta nu este în director curent. Şirul vid defineşte o căutare în directorul curent. Exemple: 6.5. GraphDriver:=EGA; GraphMode:=EGALo; InitGraph ('GraphDriver, GraphMode,'); 6.6. GraphDriver:=Detect; GraphMode:=0; InitGraph (GraphDriver, GraphMode,'C:\TP\BIN'); c) Să verifice dacă iniţializarea modului grafic dorit, din pasul anterior, s-a desfăşurat cu succes. În acest sens, se utilizează funcţia specială GraphResult, de tipul INTEGER şi fără parametri, care returnează valoarea zero (GrOK) pentru succes. Funcţia poate fi utilizată după execuţia oricărei operaţii grafice. d) Să apeleze subrutinele grafice necesare, dacă iniţializarea s-a derulat cu succes. e) Să revină în modul text, la terminarea operaţiilor grafice, prin utilizarea procedurii fără parametri, CloseGraph. Iniţializarea sistemului grafic alocă memoria dinamică necesară, iar închiderea o eliberează.

104

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

6.4.1 Fereastră şi vizor În grafica computerizată se consideră că înregistrarea pe ecran este o transpunere, micşorată sau mărită, a desenelor din spaţiul real (spaţiul utilizator). Partea imaginii reale care se desenează este cuprinsă într-un dreptunghi, numit fereastră (window), cu laturile paralele cu axele de coordonate ale acestui spaţiu. Fereastra se proiectează pe spaţiul ecran într-un dreptunghi, asemenea cu fereastra, denumit vizor (viewport). Astfel, pe ecran, pot coexista mai multe imagini independente. La limită, vizorul poate fi întregul ecran. Relaţia între fereastră şi vizor este evidenţiată în figura 6.6. De aici se deduce o transformare de coordonate, care proiectează orice punct (xr,yr) din spaţiul real, în punctul (xe,ye) al spaţiului ecran. Dacă fereastra şi vizorul se definesc prin coordonatele colţurilor stânga-sus şi dreapta-jos, iar centrele celor două dreptunghiuri sunt notate ( x 0r , y 0r ) , ( x e0 , y e0 ) , din asemănarea imaginii din cele două spaţii se pot scrie relaţiile:

x e − x 0e v − v1  = 2  0 x r − x r w 2 − w1   y e − y 0e v 4 − v3  = y 0r − y r w 3 − w 4 

(1)

x (v1,v3) y



(x 0e , y e0 ) • (xe,ye)

y

vizor

(v2,v4)

(w1,w3) •

(x 0r , y 0r ) fereastră

• (xr,yr)

x (w2,w4) Fig. 6.6 Relaţia fereastră-vizor

105

Reprezentarea vizuală a datelor

Din (1) se explicitează coordonatele (xe,ye) şi se obţin relaţiile:

v − v1 0  v 2 − v1 xr − 2 xr w 2 − w1 w 2 − w 4   v − v3 v − v3 0  y e = y 0e − 4 yr + 4 yr w3 − w4 w 3 − w 4 

x e = x 0e +

(2)

din care, prin înlocuirea coordonatelor centrelor ferestrelor şi vizorului, în funcţie de coordonatele colţurilor acestora, se obţin relaţiile:

v1 + v 2 v − v1 v − v1 w 1 + w 2  + 2 ⋅ xr − 2  2 w 2 − w1 w 2 − w1 2   (3) v3 + v4 v4 − v3 v4 − v3 w 3 + w 4  − ⋅ ye = yr +  2 w3 − w4 w3 − w4 2

xe =

Realizând calculul din (3) se obţin relaţiile:

v 2 − v1 v w − v 2 w1 xr + 1 2 w 2 − w1 w 2 − w1

    (4) v 4 − v3 v4 w 3 − v3w 4  ye = − yr + w3 − w4 w 3 − w 4 

xe =

Notând:

v − v3 v 2 − v1 > a2 = − 4 w 2 − w1 w3 − w4

    (5) v 4 w 3 − v3w 4  v1 w 2 − v 2 w 1 b1 = > b2 = w 2 − w1 w 3 − w 4 

a1 =

se obţin relaţiile de transformare: x e = Round(a 1 x r + b 1 )

(6)

y e = Round(a 2 y r + b 2 )

Relaţiile (6) dau coordonatele absolute, în spaţiul ecran, pentru orice punct (xr,yr). Este, de multe ori, preferabil să se exprime punctele în spaţiul ecran relativ la vizor, adică într-un subspaţiu cu originea în colţul stânga-sus al acestuia. Dacă se notează (dxe,dye) coordonatele absolute (xe, ye), se obţin relaţiile:

x e = v 1 + dx e   y e = v 3 + dy e 

(7)

106

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Utilizând relaţiile (4), din (6) se obţin relaţiile:

v 2 − v1  (x r − w 1 )  w 2 − w1   (8) v4 − v3 dy e = − ( y r + w 3 )  w3 − w4 dx e =

Dacă se notează: k k

1

2

=

v w

= −

2 2

v w

− v1 − w 1 4 3

− v3 − w 4

atunci se pot considera coordonatele relative sub forma:

dx e = round(k 1 ( x r − w 1 ))   (9) dy e = round(k 2 ( y r − w 3 )) Utilizarea ferestrei şi/sau vizorului în tehnica realizării imaginilor grafice oferă, în plus, posibilitatea de verificare a apartenenţei punctelor la desenul curent. Se pot implementa măsuri de eliminare a punctelor străine (tehnica clipping-ului sau tăierii), pentru a asigura protecţia imaginilor vecine împotriva distrugerilor accidentale, desenându-se numai imaginea din fereastră. În sistemul grafic Pascal, ideile tehnicii fereastră-vizor sunt implementate parţial, aşa după cum rezultă din cele ce urmează. • Există posibilitatea definirii vizorului ca spaţiu curent pentru un anumit desen (vizor curent). Declararea lui se face prin apelul procedurii SetViewport, în forma: SetViewport(v1,v3,v2,v4,Clip); în care (v1,v3) şi (v2,v4) sunt constante sau variabile de tipul INTEGER care dau coordonatele colţurilor vizorului, iar Clip este un parametru boolean care activează sau inhibă aplicarea clipping-ului. Unitatea Graph defineşte funcţiile de tipul INTEGER fără parametri: GetMaxX şi GetMaxY care returnează dimensiunile maxime ale spaţiului ecran pentru placa grafică cu care este echipat calculatorul. Pentru a crea independenţă programului faţă de calculator, se recomandă utilizarea lor în definirea vizorului. De asemenea, unitatea Graph defineşte constantele simbolice ClipOn (True) şi ClipOff (False) care pot fi folosite pentru parametrul Clip.

107

Reprezentarea vizuală a datelor

Exemplu: 6.7. SetViewport(10,25,GetMaxX-150,GetMaxY-50,ClipOn); Vizorul implicit, stabilit la iniţializarea modului grafic, este întregul ecran, echivalent cu un apel de forma: SetViewport (0,0,GetMaxX,GetMaxY,ClipOn) ; • Vizorul curent poate fi şters, la culoarea de font curentă, prin apelul procedurii ClearViewport, care este fără parametri. Pentru a fi pus în evidenţă faţă de zonele vecine, vizorul poate fi şters cu o culoare de fond adecvată, stabilită în prealabil (vezi § 6.4.2.) şi poate fi încadrat într-un dreptunghi.

Exemplu: 6.8. Pentru un ecran monocrom (dar nu numai) se poate utiliza o secvenţă de forma: Rectangle(9,24,GetMaxX-149,GetMaxY-49); SetViewport(10,25,GetMaxX-150,GetMaxY-50,ClipOn) ClearViewport; în care procedura Rectangle desenează un dreptunghi cu liniile în culoarea curentă de desen (alb), iar SetViewport îl şterge, la culoarea fondului. Procedura ClearDevice şterge întregul ecran şi determină revenirea la parametrii de lucru impliciţi, stabiliţi la iniţializare. • Declararea vizorului determină rutinele sistemului grafic să lucreze în coordonate relative. Aceasta înseamnă că orice procedură sau funcţie care primeşte ca parametru de intrare coordonatele (dxe,dye) ’înţelege’ să le utilizeze pentru a calcula coordonatele absolute (xe,ye), prin relaţii de forma (6), iar cele care au aceste coordonate ca parametru de ieşire le determină prin relaţiile: dx e = x e − v1  

dy e = y e − v 3  • Sistemul nu prevede posibilitatea declarării ferestrelor în spaţiul utilizator

şi, de aici, lipsa facilităţii de exprimare a coordonatelor, pentru rutinele de grafică, direct în acest spaţiu şi transformarea lor automată în coordonatele ecran. Programatorul trebuie să-şi construiască proceduri proprii care să-i transforme coordonatele din spaţiul utilizator în spaţiul ecran, relative la vizorul curent. Deşi pot fi aplicate procedee diverse, după natura desenului, pentru generalitate şi uniformitate în realizarea programelor de grafică se aplică tehnica fereastră-vizor descrisă anterior.

108

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

6.4.2 Grafică în culori Sistemul grafic Pascal implementează culorile pe principiul paletei. Sistemul posedă o serie de proceduri, funcţii şi date prin care se pot accesa facilităţile de culoare ale plăcii grafice şi modului grafic selectat. • Determinarea paletei. Deoarece caracteristicile de culoare depind de placa grafică şi modul grafic selectat, se poate obţine un program relativ portabil, dacă acesta se scrie astfel încât să-şi determine singur facilităţile pe care le poate utiliza. Pentru aceasta pot fi utilizate rutinele: - GetMaxColor - funcţie fără parametri care întoarce o valoare de tipul WORD, reprezentând indexul maxim al paletei; - GetPaletteSize - funcţie fără parametri care întoarce o valoare de tipul INTEGER reprezentând numărul maxim de culori care pot fi utilizate simultan; - GetDefaultPalette(paleta) - procedură care returnează, în parametrul paleta, numărul şi culorile paletei implicate; - GetPalette(paleta) - procedură care returnează aceleaşi date ca procedura anterioară, dar pentru paleta curentă. Parametrul paleta trebuie să fie declarat ca o variabilă de tipul PaletteType, definit în unitatea Graph ca un articol cu câmpurile: Size (Byte) şi Colors (Shortint). • Modificarea paletei curente. Programatorul poate modifica una sau toate intrările paletei curente, dar pantru aceasta trebuie să cunoască codurile de culoare. Procedurile care se utilizează sunt: - SetPalette(Index,Color) - care permite modificarea intrării date de parametrul Index (WORD), cu o culoare de cod Color (INTEGER). Atât pentru Index, cât şi pentru Color pot fi utilizate constantele simbolice predefinite în unitatea Graph. - SetAllPalette(Palette) - înlocuieşte paleta curentă cu cea dată de parametrul Palette. Parametrul este de tipul PaletteType şi trebuie să fi fost încărcat corect cu codurile de culori. În ambele situaţii, procedurile modifică automat culorilor desenului de pe ecran, în concordanţă cu noua configuraţie a paletei curente. Procedurile pot fi utilizate şi cu scopul de a produce efecte de culoare deosebite sau pentru a releva treptat, prin modificări repetate, un desen ’ascuns’. • Determinarea şi modificarea culorilor de fond şi de desen. Funcţiile şi procedurile din această categorie sunt cel mai frecvent utilizate deoarece definesc fondul şi culoarea de desen pe baza paletei curente. -SetBkColor(Color) - procedură pentru selecţia culorii de fond. Color este parametrul de tip WORD care precizează indexul culorii dorite. Implicit, se utilizează indexul zero care corespunde, în general, culorii negre; - GetBkColor - funcţie fără parametri, de tipul WORD, care returnează indexul culorii curente pentru fond;

109

Reprezentarea vizuală a datelor

- SetColor(Color) - procedură pentru selecţia culorii de desen. Color are aceeaşi semnificaţie ca la procedura anterioară. Implicit, se utilizează ultimul Index care, în general, corespunde culorii alb; - GetColor - funcţie similară cu GetBkColor pentru a obţine culoarea curentă de desen. Modificarea culorii de fond atrage după sine modificarea culorii pe ecran. Culorile selectate, denumite culori curente, rămân active până la o nouă selecţie sau până la ieşirea din program. Exemplu: 6.9. Presupunând o placă EGA, secvenţa care urmează stabileşte un vizor cu fond albastru în care desenează un dreptunghi umplut. După 2000 ms se refac culorile anterioare. Uses Graph,Crt; VAR ColorBk,ColorFg:Word; …………………….. ColorBk:=GetBkColor; ColorFg:=GetColor; SetViewport(20,50,250,150,ClipOn); SetColor(EGARed); SetBkColor(EGABlue); ClearViewport; Bar(20,10,60,50); Delay(2000); SetBkColor(ColorBk); SetColor(ColorFg); . . . . . . . . . . . . . .

6.4.3 Desen prin puncte şi vectori Desenul prin puncte este modul ’natural’ de realizare a imaginii video. Biblioteca de subprograme grafice Pascal conţine pentru acest mod de lucru procedura PutPixel(x,y,Color) şi funcţia de tipul WORD GetPixel(x, y). Procedura PutPixel stabileşte culoarea Color de aprindere a pixelului de coordonate (x,y) din vizorul curent, iar funcţia GetPixel întoarce indexul culorii cu care este aprins un astfel de pixel. Pe baza subprogramelor, se pot construi desene complexe, dar cu dificultăţile pe care le implică gestionarea mulţimii de pixeli care compun imaginea video. Pentru a facilita realizarea desenelor, sistemul grafic PASCAL implementează şi alte moduri de lucru, cum este desenul prin vectori. În acest mod, pot fi trasate segmente de dreaptă, independente sau legate (linii frânte), utilizând pentru forma liniilor stilurile şi culorile curente (setate). Culoarea de desen şi culoarea fondului sunt curente, setate prin SetBkColor şi SetColor. Stilul de linie este cel definit de procedura SetLineStyle,care se apelează astfel: SetLineStyle(TipStil,Model,Grosime);

110

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Stilul definit rămâne valabil până la o nouă setare. Stilul implicit este linia continuă normală. Parametrii, de tipul WORD, constante sau variabile, definesc atributele liniei (stilul liniei): - TipStil - defineşte tipul de linie din punctul de vedere al continuităţii. Parametrul poate avea valorile 0-4 sau constanta simbolică corespunzătoare; - Grosime - precizează grosimea, în număr de pixeli şi poate avea valorile: 1 (linie normală - NormWidth) şi 2 (linie groasă - ThickWidth); - Model - defineşte un stil de utilizator printr-un fragment de 16 pixeli. Un pixel este aprins, dacă bitul corespunzător, din numărul dat de acest parametru, este unu. De exemplu, dacă Model=$F053 (în binar 1111 0000 0101 0011) atunci se defineşte modelul •••• xxxx x•x• xx••, în care • este pixel aprins. Desenarea unui segment de dreaptă se face prin procedura Line(x1,y1,x2,y2). Prin parametrii x1, y1, x2, y2, de tip INTEGER, se precizează coordonatele capetelor segmentului de dreaptă. Coordonatele sunt relative la vizorul curent şi se pot exprima ca variabile sau constante. Se desemnează numai porţiunea de segment care se află în interiorul vizorului. Exemplu: 6.10. Secvenţa care urmează: SetViewport(20,20,150,100,ClipOn); Line(10,10,150,150);

desenează un segment cu originea absolută în (30,30) şi extremitatea finală în (170,120) din care se vede numai porţiunea de coordonate absolute (30,30), (138,100) - figura 6.7. Desenarea segmentelor de dreaptă cu procedura Line este incomodă, când trebuie desenate linii frânte, datorită redundanţei de informaţii, având în vedere coincidenţa punctului final al unui segment cu originea segmentului următor. Desenarea liniilor frânte este facilitată de introducerea în sistemul grafic a noţiunii de punct curent şi de existenţa unor rutine care desenează linii relativ la acesta. Punctul curent, definit prin coordonatele sale în spaţiul ecran, este înţeles de rutinele sistemului ca origine a următorului segment de dreaptă posibil de desenat. Sistemul prevede rutine pentru aflarea şi modificarea poziţiei punctului curent, precum şi reguli de comportare a rutinelor de trasare de linii, cu privire la actualizarea acestuia. Locul punctului curent poate fi aflat cu ajutorul funcţiilor, fără parametri, GetX şi GetY, care returnează abscisa şi ordonata absolută, ca numere întregi.

111

Reprezentarea vizuală a datelor x

(0,0) (20,20) • (30,30) •

(138,100 •

• (150,100) • (170,120)

y

Fig. 6.7. Desenarea liniilor relativ la vizor

Punctul curent poate fi mutat într-un alt loc din spaţiul fizic, prin procedurile MoveTo şi MoveRel care se apelează sub forma: MoveTo(x,y); MoveRel(x,y). Procedura MoveTo consideră valorile (x,y) drept coordonate relative la vizorul curent şi calculează coordonatele absolute (xe,ye) ale punctului curent prin relaţiile: xe=v1+x; ye=v3+y, unde (v1,v3) sunt coordonatele originii vizorului. Procedura MoveRel deplasează punctul curent relativ la poziţia sa anterioară, adică xe=GetX+x; ye=GetY+y, în care x,y apar ca deplasări. La intrarea în regim grafic, la definirea şi/sau ştergerea vizoarelor, precum şi la schimbarea modului grafic, punctul grafic curent este nedefinit. Este sarcina programului să iniţializeze punctul curent prin procedura MoveTo. Toate procedurile de desenare de linii şi scriere de text deplasează poziţia punctului curent în ultimul punct utilizat. Desenarea liniilor care îşi au originea în punctul curent se realizează cu ajutorul procedurilor LineTo(x,y); LineRel(x,y). Pentru LineTo, coordonatele extremităţii liniei sunt relative la vizorul curent şi deci xe=v1+x, ye=v3+y, unde (xe,ye) sunt coordonatele absolute ale extremităţii liniei relativ la punctul curent xe=GetX+x; ye=GetY+y. După trasare, punctul curent este deplasat în punctul xc=xe, yc=ye. Dacă la trasare a fost necesară operaţia de clipping, punctul final al segmentului vizibil este recalculat ca (x ~ , y ~ ) , însă punctul curent este mutat tot în (xe,ye). Se evită astfel e

e

deformarea desenelor. Exemplu: 6.11. Dacă se consideră linia frântă P1, P2, P3, P4 din figura 6.8, atunci secvenţele de instrucţiuni de trasare, având în vedere coordonate relative la vizor şi relative la punctul curent, pot fi scrise astfel: Relativ la vizor MoveTo(50,60) LineTo(70,40) LineTo(85,45) LineTo(140,30)

Relativ la punctul curent MoveTo(50,60) LineRel(20,-20) LineRel(15,5) LineRel(25,-15)

112

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

(0,0)

x

(20,10) (110,30) [25,-15]

(70,40) [20,-20]







(50,60)



(85,45) [15,5]

y

Fig. 6.8. Desenarea segmentelor relativ la vizor şi la punctul curent În plus, trebuie menţionat faptul că desenarea unei linii simple sau frânte poate fi făcută astfel încât să distrugă sau să conserve, într-un anumit sens, culorile pixelilor care se suprapun. Comportamentul pixelilor depinde de modul de scriere a noii imagini în raport cu imaginea existentă pe ecran (setarea modului Write);

6.4.4 Raportul de aspect În realizarea desenelor prin segmente apare un fenomen de deformare care poate să fie deranjant (un pătrat apare ca dreptunghi, un cerc ca o elipsă etc). Fenomenul se datorează formei pixelilor care, cu excepţia unor plăci de foarte mare rezoluţie, nu sunt de formă pătratică, ci dreptunghiulară. Aceasta face să se aplice, implicit, unităţi diferite de măsură pe cele două axe. Deformarea poate fi atenuată sau chiar eliminată, dacă în realizarea desenului se aplică un anumit raport între lungimile segmentelor de pe orizontală şi ale celor de pe verticală. Dacă se notează cu Lpx lungimea unui pixel, în sensul axei ox şi cu Ipy înălţimea sa, în sensul axei oy, atunci raportul Ra=Ipy/Lpx este denumit raport de aspect. El poate fi utilizat pentru a determina, în mod adecvat, lungimile segmentelor (în pixeli) care se desenează. Pentru a obţine dimensiunile pixelului plăcii grafice se utilizează procedura: GetAspectRatio(Lpx, Ipy); în care cei doi parametri sunt variabile de tipul WORD. Dacă se notează cu AB un segment vertical de lungime n pixeli şi cu CD un segment orizontal, de lungime m pixeli, atunci, pentru ca raportul AB/CD să fie k, trebuie ca m, dacă se cunoaşte n (respectiv n, dacă se cunoaşte m), să se determine cu relaţiile: m = Round(Ra * n k )   n = Round(m * k Ra ) 

113

Reprezentarea vizuală a datelor

Exemplu: 6.12. Desenarea unui pătrat, cu latura orizontală CD de m=50 pixeli şi vârful stânga sus în (30,60), pe un vizor egal cu întregul ecran. Deoarece k=1, se poate utiliza secvenţa: GetAspectRatio(Lpx,Ipy); Ra:=Ipy/Lpx; n=Round(50/Ra); MoveTo(30,60); LineTo(80,60); LineTo(80,60+n); LineTo(30,60+n); LineTo(30,60);

Există, de asemenea, rutina SetAspectRatio care permite definirea software a raportului de aspect pentru a fi utilizat de rutinele sistemului grafic. Programatorul care utilizează rutinele sistemului pentru desenare de figuri poate să reducă fenomenul de deformare, modificând repetat, eventual interactiv, raportul de aspect. Apelul acestei proceduri este similar procedurii GetAspectRatio.

6.4.5 Scrierea textelor în modul grafic În mod uzual, imaginile de pe ecran sunt însoţite de texte explicative. Având în vedere că, de multe ori, lucrul în modul text nu satisface cerinţele de mărime, formă, culoare şi direcţie de scriere, în unit-ul Graph au fost introduse primitive specifice prelucrării textelor. Unitatea conţine două grupe de rutine: pentru definirea atributelor textelor şi pentru scrierea propriu-zisă. • Rutinele de declarare a atributelor textelor trebuie apelate ori de câte ori este necesar să se definească alte caracteristici de scriere decât cele curente. Atributele astfel definite (setate) rămân active până la o nouă setare sau până la sfârşitul programului. Rutinele de declarare sunt proiectate pentru a putea trata următoarele atribute de scriere: a) Stilul de caractere, adică forma caracterelor imprimabile. Sistemul recunoaşte cinci stiluri (fonturi), codificate 0-4, cărora li s-au asociat constante simbolice; b) Culoarea: caracterele se afişează pe fondul şi culoarea de desen curentă; c) Mărimea se referă la lăţimea şi înălţimea caracterelor, caracteristici care se definesc pentru toate fonturile, cu excepţia stilului DefaultFont, la care mărimea nu poate fi modificată. Fonturile cu mărime de caracter modificabilă definesc caracterele prin vectori (caractere vectoriale). Mărimea se poate referi la spaţiul dreptunghiular pentru un caracter, în sensul micşorării sau creşterii proporţionale a acestuia pentru ambele dimensiuni sau pentru fiecare dimensiune în parte (dimensiuni de utilizator).

114

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

d) Direcţia de scriere poate fi: orizontală sau verticală. Pentru direcţia orizontal (constanta simbolică HorizDir) scrierea se face de la stânga spre dreapta, iar pentru direcţia vertical (VertDir), de jos în sus; e) Alinierea: precizează, pentru fiecare din cele două direcţii, modul de amplasare a textului în raport cu punctul de scriere (punct de referinţă). Pentru definirea atributelor se pot utiliza procedurile: SetTextStyle (Font, Directie, Marime); SetTextJustify (FtHorizDir, FtVertDir); SetUserCharSize (MultX, DivX, MultY, DivY); Parametrul de mărime, la procedura SetTextStyle, poate lua valori naturale mai mari sau egale cu zero şi precizează de câte ori să fie mărite, proporţional, caracterele fontului ales faţă de dimensiunea normală a acestora. Pentru definirea dimensiunilor de utilizator (independente pe cele două direcţii), valoare zero a parametrului Marime declară intenţia programatorului de a apela procedura SetUserCharSize. La această procedură, se utilizează ideea că dimensiunea se modifică prin înmulţire cu un raport. Raportul MultX/DivX va fi utilizat pentru a modifica lăţimea caracterelor, iar MultY/DivY pentru modificarea înălţimii. Atunci când raportul este unu, dimensiunea pe direcţia respectivă va rămâne nemodificată. La procedura de definire a alinierii se cere să se precizeze modul de aliniere a textului pe ambele direcţii, chiar dacă scrierea utilizează numai una din ele. Exemple: 6.13. Se defineşte fontul SmallFont, cu mărire proporţională, de 3 ori şi cu afişare centrată pe orizontală: SetTextJustify(CenterText,CenterText); SetTextStyle(SmallFont,HorizDir,3);

6.14. Se stabileşte o creştere a dimensiunii caracterelor fontului SansSerifFont numai în înălţime, de 1,5 ori; scrierea se face pe direcţia verticală, cu aliniere la partea de jos: SetTextStyle(SansSerifFont,VerDir,0); SetUserCharSize(1,1,3,2); SetTextJustify(LeftText,BottomText);

Dacă atributele de scriere au fost deja definite, adică au devenit atribute curente, programatorul poate utiliza funcţiile întregi (de tipul WORD): TextHeight(Text), TextWidth(Text), pentru a afla înălţimea, respectiv lăţimea, în număr de pixeli, necesare pentru a afişa întregul text dat de parametrul Text. Pe baza acestor informaţii, se poate alege punctul de referinţă astfel încât scrierea textului să fie posibilă (totul să înceapă în vizorul de scriere) sau acesta să fie mai bine amplasat, pe direcţia curentă.

115

Reprezentarea vizuală a datelor • Pentru scrierea efectivă a textelor se utilizează una din următoarele două proceduri: OutText(Text); OutTextXY(x,y,Text); Textul de scris (parametrul Text) se poate prezenta ca o constantă, variabilă sau expresie de tipul STRING. Scrierea se face în raport cu punctul de referinţă cu coordonate (x,y) sau “la cursor”, dacă nu se defineşte explicit un astfel de punct. După scriere, cursorul se găseşte pe poziţia următoare celei utilizate pentru scrierea ultimului caracter al textului, dacă scrierea s-a făcut prin procedura OutText sau rămâne nemodificat, atunci când s-a utilizat procedura OutTextXY. Dacă există definit vizor curent, cu acceptarea clipping-ului, textul este decupat la limitele vizorului. Nu se aplică procedeul de clipping dacă fontul este DefaultFont şi comportarea rutinei de scriere este nedefinită când survine o depăşire a vizorului. În plus, trebuie menţionat că primitivele de afişare ţin cont de setarea modului de scriere.

Exemplu: 6.15. Se scrie orizontal textul "TURBO PASCAL", de 4 ori, cu cele patru fonturi vectoriale. Fiecare rând, centrat pe linia verticală a centrului ecranului, se scrie cu o altă culoare. Clear Device; SetTextJustify(CenterText,CenterText); y:=20; For i:=1 to 4 do Begin SetColor (i); SetTextStyle(i,HorizDir,2+i); Y:=y+TextHeight('TURBO PASCAL')+8; OutTextXY(GetMaxX Div 2,Y,'TURBO PASCAL'); End;

6.4.6 Primitive pentru figuri Pentru a facilita realizarea imaginilor, sistemul grafic cuprinde câteva primitive pentru desenarea unor figuri, majoritatea în plan. Unele din aceste proceduri umplu figura desenată cu un anumit model de culoare şi haşură. Toate rutinele au fost concepute să lucreze în condiţiile existenţei unei vizor curent, adică în coordonate relative la vizor şi să aplice clipping-ul, dacă acesta a fost acceptat. Câteva din ele ţin seama de modul de scriere selectat. Ca şi rutinele prezentate anterior, primitivele pentru figuri utilizează, pentru contur şi linii, culorile şi atributele curente. Dacă o primitivă umple figura, atunci ea foloseşte un model de haşurare şi o anumită culoare. Modelul de haşurare poate fi ales dintr-o mulţime predefinită sau poate fi definit de utilizator.

116

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Pentru a preciza stilul şi culoarea de umplere se apelează procedura: SetFillStyle(TipStil,Culoare); în care parametrul Culoare este un index în paleta curentă, iar TipStil o constantă între 0-12. Dacă TipStil este 12 (UserFill), atunci el trebuie să fie definit ca un model binar pe 8 byte (un vector) care este interpretat de rutinele respective ca o matrice de 8x8 biţi. Matricea este traversată circular pe linii şi pixelul curent este aprins, la culoarea definită prin parametrul culoare, dacă bitul din model este unu. În plus, tipul definit rămâne ca model curent. Exemplu: 6.16. CONST MyPattern:Array[1..8]of Byte=($01,$01,$FF,$01,$01,$FF,$01,$01); . . . . . . . . . . SetFillStyle(MyPattern,3);

În figura 6.9 se prezintă toate tipurile de haşuri considerând numai culorile alb şi negru.

Empty

Solid

Line

LtSlash

Slash

BkSlash ••••••• ••••••• ••••••• •••••••

Hatch

XHatch

Interline

Wide Dot

Close Dot

User

Fig. 6.9 Tipuri de haşuri Principalele primitive pentru figuri sunt prezentate în continuare. a) Proceduri pentru dreptunghiuri: Rectangle (x1,y1,x2,y2); Bar(x1,y1,x2,y2); Prima procedură desenează un dreptunghi simplu, neumplut, iar a doua un dreptunghi umplut. Procedura Rectangle ţine seama de setarea modului de scriere (WriteMode). Parametrii de apel sunt coordonatele întregi ale colţului stânga-sus (x1,y1) şi dreapta jos (x2,y2) ale dreptunghiului. b) Proceduri pentru linii poligonale şi poligoane oarecare:

117

Reprezentarea vizuală a datelor

DrawPoly(NumarVarfuri,CoordonateVarfuri); FillPoly(NumarVarfuri,CoordonateVarfuri); Procedurile sunt similare perechii de proceduri pentru desenarea dreptunghiurilor. Coordonatele punctelor liniei poligonale trebuie să fie declarate ca o variabilă de tip masiv, la care tipul de bază trebuie să fie PointType, adică un articol cu două câmpuri: X şi Y. Dacă se doreşte trasarea unui poligon, atunci faţă de linia poligonală cu n vârfuri trebuie declarate n+1 vârfuri şi coordonatele ultimului vârf trebuie să coincidă cu cele ale primului. Procedura FillPoly se aplică numai pentru poligoane, iar interiorul acestora este umplut cu haşura şi culoarea cerute. Exemplu: 6.17. CONST Pentagon: Array[1…6]of PointType= ((x:20;y:45),(x:50;y:25),(x:100;y:10), (x:120;y:40),(x:80;y:60),(x:20;y:45)); FillPoly (6,Pentagon);

c) Proceduri pentru arce, cercuri şi elipse. Procedurile din această categorie desenează marginile figurilor utilizând culoarea curentă, stabilită de SetColor. Cele care haşurează sau umplu figura desenată utilizează atributele date de SetFillStyle. Procedurile nu ţin seama de setarea modului de scriere. Procedurile care desenează arce utilizează, ca parametri, unghiuri în grade. Trebuie avut în vedere că acestea sunt considerate în sens trigonometric (sens invers acelor de ceasornic, cu 0 grade la ora 3, 90 grade la ora 12, 180 grade la ora 9 etc.). De asemenea, raza cercului şi razele elipselor (raza orizontală - RazaX şi raza verticală - RazaY) se dau în număr de pixeli. Coordonatele centrului figurii constituie, de asemenea, un parametru (x,y). Pentru asigurarea circularităţii pe ecran, procedurile utilizează raportul de aspect al plăcii, eventual setat soft, prin SetAspectRatio. Procedurile de bază sunt: Arc (x,y,UnghiStart,UnghiFinal,Raza); PieSlice (x,y,UnghiStart,UnghiFinal,Raza); Circle (x,y,Raza); Ellipse ((x,y,UnghiStart,UnghiFinal,RazaX,RazaY); Sector (x,y,UnghiStart,UnghiFinal,Raza); FillEllipse (x,y,RazaX,RazaY);

118

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Procedura Circle desenează un cerc (neumplut), iar procedura Arc desenează un arc care, la limită, poate fi un cerc, depinzând de alegerea unghiurilor. Procedura PieSlice desenează un sector umplut care poate fi, la limită, un cerc umplut. Aproape similar este cazul elipselor. Procedura Ellipse desenează un arc de elipsă sau o elipsă (neumplută), procedura Sector desenează un sector de elipsă sau o elipsă umplută, iar procedura FillEllipse desenează o elipsă umplută. Exemplu: 6.18. Arc (150,100,0,270,50); PieSlice (GetMaxX Div 2,GetMaxY Div 2,0,360,GetMaxX Div 4); FillEllipse (150,150,300,75).

Trebuie remarcat şi faptul că sistemul grafic posedă o procedură de interogare asupra centrului punctului de start şi punctului final care au fost utilizate în timpul execuţiei uneia din procedurile: Arc, PieSlice, Ellipse, Sector. Aceste informaţii sunt furnizate prin apelul procedurii: GetArcCoords(ArcCoords); în care parametrul ArcCoords are tipul ArcCoordsType definit public în unitatea Graph astfel: Type ArcCoords=Record {Punct de centru} x,y:Integer; xStart,yStart:Integer; {Punct de inceput} {Punct de sfarsit} xEnd,yEnd:Integer; End; Procedura este utilă pentru racordarea arcelor cu segmente de dreaptă sau alte arce. d) Procedura pentru paralelipiped dreptunghic

Bar3D (x1,y1,x2,y2,Grosime,Top); Procedura permite desenarea paralelipipedelor dreptunghice colorate (umplute). Parametrii au următoarele semnificaţii: - (x1,y1),(x2,y2) sunt coordonatele colţurilor stânga-sus, dreapta-jos ale feţei (dreptunghiului din faţă); - Grosime, de tipul WORD, indică grosimea paralelipipedului; - Top, de tip BOOLEAN, arată că faţa superioară trebuie să fie vizibilă (valoare True sau constanta TopOn) sau să fie ascunsă (valoare False sau TopOff). Parametrul permite realizarea paralelipipedelor suprapuse, caz în care trebuie să fie vizibilă numai faţa superioară a ultimului paralelipiped. Procedura se poate utiliza pentru desenarea histogramelor prin bare (grafice din bare) care prezintă, comparativ, valorile unei (unor) caracteristici (mărimi).

119

Reprezentarea vizuală a datelor

6.4.7 Elemente de animaţie Animaţia poate fi considerată ca tehnică de realizare a mişcării obiectelor grafice pe ecran. Ea presupune refacerea imaginii video, de peste 30 de ori pe secundă, cu obiectele în diferite poziţii, astfel încât să se asigure persistenţa imaginii pe retina ochiului. Reuşita în realizarea animaţiei este strâns condiţionată de resursele calculatorului, cum sunt: viteza microprocesorului, existenţa coprocesorului matematic, numărul de pagini ale memoriei video etc. 1. Aspecte generale. Animaţia este o tehnică complexă prin multitudinea de probleme pe care le pune realizatorului de programe. • În primul rând, programul trebuie să ia în considerare comportarea obiectului de animat în “mediu”. În cazurile simple, imaginea pe ecran se reduce la obiectul în mişcare, adică nu există mediu ambiant. Uzual, însă, pe ecran există o imagine “peste” care trebuie să se deplaseze obiectul. Această imagine este, pentru obiectul animat, un mediu ambiant, pe care acesta trebuie să îl conserve. Rezultă de aici că imaginea obiectului, în diferitele sale poziţii, trebuie scrisă peste imaginea mediu într-un anumit mod. De regulă, regimul grafic al unui calculator acceptă cel puţin două moduri de scriere (WriteMode): unul care distruge imaginea veche (modul PUT) şi altul care suprapune noua imagine peste cea veche (modul XOR). Trebuie remarcat că realizarea a două operaţii logice XOR (sau exclusiv) succesive ale unei imagini peste acelaşi mediu înseamnă, de fapt, ştergerea noii imagini din poziţia respectivă. • În al doilea rând, programul creşte în complexitate dacă obiectul animat nu rămâne identic cu el însuşi în toate poziţiile sale de mişcare. Este mai simplu de animat un obiect pentru care imaginea formei sale se conservă, deoarece această imagine poate fi realizată o singură dată, memorată şi, apoi, afişată în diferite poziţii. Dacă obiectul animat îşi schimbă forma de la o poziţie la alta, aşa cum este cazul general, atunci imaginea sa trebuie refăcută continuu. În acest caz, consumul de timp este mult mai mare şi animaţia reuşeşte numai în prezenţa unor resurse suplimentare ale calculatorului care să asigure viteza necesară de afişare. • În al treilea rând, obiectul în mişcare poate să aibă un anumit comportament la atingerea limitelor vizorului de afişare. Problema este simplă atunci când obiectul poate ieşi şi intra în vizor, deoarece se rezolvă prin tehnica clipping-ului. Dacă obiectul nu poate părăsi vizorul sau atingerea limitelor acestuia îi imprimă o anumită traiectorie, atunci programul trebuie să prevadă teste pentru a sesiza situaţia şi să includă relaţii adecvate de calcul. • În sfârşit, probleme complexe ridică situaţiile în care mediul însuşi poate modifica forma şi/sau traiectoria obiectului sau când trebuie animate mai multe obiecte concomitent. Din punct de vedere algoritmic, orice program de animaţie trebuie să aibă încorporaţi următorii paşi: a) Se afişează pe ecran imaginea, cu obiectul în poziţia a; b) Se construieşte imaginea cu obiectul într-o poziţie nouă, b (dacă este cazul); c) Se şterge obiectul din poziţia a (eventual, prin afişarea a doua oară a imaginii acestuia în a, cu scriere XOR);

120

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

d) Se atribuie lui a valoarea lui b şi se reia, dacă este cazul, de la punctul a. 2. Facilităţi pentru realizarea animaţiei în PASCAL. Sistemul grafic Turbo Pascal prevede câteva rutine care facilitează realizarea animaţiei. • Moduri de scriere. Pot fi definite moduri de determinare a culorii unui pixel din noua imagine, în funcţie de culoarea curentă de scriere şi culoarea pe care acelaşi pixel o are în imaginea existentă pe ecran (culoarea veche). Având în vedere că noua imagine se constituie în memoria video, peste cea existentă, sunt posibile diferite operaţii logice între biţii care determină culoarea unui pixel şi noile valori dorite. Aceste operaţii sunt denumite moduri de scriere şi se definesc prin procedura: SetWriteMode(Mode); unde parametrul Mode poate lua valorile întregi între 0-4. În mod implicit se aplică NormalPut, când pixelul capătă culoarea curentă, indiferent de culoarea pe care o avea (imaginea veche dispare). Cel mai frecvent se utilizează XOR, care determină culorile după o funcţie ”sau exclusiv”. Se are în vedere că specificarea culorilor se face prin index în paletă şi, deci, rezultatul este tot o astfel de valoare. Din exemplul următor se poate urmări şi afirmaţia că (A XOR B) XOR B = A. Culoare veche (A) 0011 (3) 1011 (11) 1111 (15) 0000 (0)

Culoare curentă (B) 0101 (5) 0010 (2) 1101 (13) 1111 (15)

Culoare nouă (A XOR B) 0110 (6) 1001 (9) 0010 (2) 1111 (15)

(A XOR B) XOR B 0011 1011 1111 0000

(3) (11) (15) (0)

Aşa după cum s-a mai remarcat, nu toate procedurile grafice ţin seama de setarea modului de scriere. Rutinele care determină culoarea nouă după regulile descrise sunt: Line LineTo LineRel OutTextXY DrawPoly Rectangle OutText • Salvarea şi restaurarea unei imagini. Sistemul prevede salvarea, din memoria ecran, a unei imagini încadrată într-o zonă dreptunghiulară, într-un buffer definit de utilizator în memoria internă, de regulă în yona heap. Se utilizează procedura: GetImage(X1,Y1,X2,Y2,Buffer); în care (X1,Y1), (X2,Y2) dau coorodnatele colţurilor stânga-sus şi dreapta-jos ale drptunghiului care defineşte imaginea de salvat. Variabila Buffer trebuie să aibă un număr acoperitor de cuvinte pentru imagine, plus două cuvinte, (la începutul acesteia) în care procedura înscrie lăţimea şi lungimea dreptunghiului de imagine. Mărimea variabilei Buffer este returnată de funcţia ImageSize(X1,Y1,X2,Y2) de tipul WORD şi nu poate depăşi 64KB. Imaginea salvată în buffer poate să fie afişată din nou utilizând procedura: PutImage (X1,Y1,Buffer,WriteMode);

121

Reprezentarea vizuală a datelor

În acest apel, (X1,Y1) sunt coordonatele punctului în care se va suprapune colţul stânga-sus al imaginii, iar WriteMode defineşte modul de scriere a imaginii şi are una din valorile definite pentru procedura SetWriteMode. Exemplu: 6.19. Se desenează un cerc umplut, cu centrul în (50,50) şi raza de 40 pixeli. Imaginea se salvează şi apoi este afişată (restaurată) în punctul (200,50), fiind ştearsă din vechea sa poziţie. Imaginea nouă conservă imaginea existentă pe ecran. . . . . . . . Uses CRT,Graph . . . . . . . p:Pointer; Sz:Word; . . . . . . . . . PieSlice(50,50,0,360,40); Sz:=ImageSize(0,0,50,100); GetMem(p,Sz); GetImage(0,0,50,100,p^); Delay(2000); PutImage(0,0,p^,XORPut); PutImage(200,50,p^,XORPut); . . . . . . . . .

• Pagina activă şi pagina video. Pentru modurile grafice care suportă mai multe pagini (EGA, VGA etc.), imaginea se poate constitui în pagina activă, în timp ce pe ecran este afişată pagina video (vezi §6.2). Pentru selecţia paginilor se utilizează procedurile: SetActivePage(NumarPagina); SetVisualPage(NumarPagina); în care parametrul NumarPagina, de tipul WORD, poate lua valori naturale, începând cu zero. Facilitatea de paginare în memoria video asigură o creştere de viteză şi uşurinţă în realizarea imaginilor, mai ales, în animaţie.

122

TEHNICI SPECIALE ÎN PASCAL

Pentru practica proiectării, punerii la punct şi dezvoltării programelor, mediul integrat Turbo Pascal oferă programatorului o serie de tehnici de lucru speciale şi instrumente puternice care să îl asiste în acest proces complex.

7.1 Tehnici de reacoperire în construirea programelor complexe Programele complexe, de dimensiuni mari (uneori peste spaţiul de memorie disponibil), pot fi concepute modularizat, într-o structură arborescentă, în care se disting un modul monitor şi mai multe module operaţionale. Modulul monitor este rezident în memorie pe parcursul execuţiei programului şi apelează succesiv modulele operaţionale care pot sau nu să fie prezente simultan în memoria principală. Reacoperirea este tehnica prin care două sau mai multe unităţi se încarcă la aceeaşi adresă de memorie, evident nu simultan. Construirea structurilor de reacoperire este posibilă numai pentru modulele constituite ca unităţi ale utilizatorului (unităţile standard nu pot face obiectul reacoperirii). În general, un program realizat cu unităţi poate fi construit ca program executabil în două forme: cu structură liniară, respectiv cu structură arborescentă. La programele cu structură liniară, spaţiul de memorie necesar este dat de suma lungimilor tuturor unităţilor, la care se adaugă lungimea programului principal. Programul, cu toate componentele sale, va fi încărcat complet în memorie şi va rămâne rezident pe tot parcursul execuţiei. La programele cu structură arborescentă se poate realiza o structură cu reacoperire (overlay), în care se disting două tipuri de componente: componenta rezidentă (rădăcina arborelui), care rămâne în memorie pe tot parcursul execuţiei programului; componentele reacoperibile (ramurile arborelui), care folosesc pe rând aceeaşi arie de memorie, în care se încarcă succesiv pe măsura referirii lor, cu eventuala suprascriere (reacoperire) a celor încărcate anterior. Componentele reacoperibile sunt formate din unităţi ale utilizatorului. Pe exemplul din figura 7.1, presupunând că partea rezidentă este formată din programul principal şi unitatea C, iar celelalte unităţi se reacoperă între ele, necesarul

123

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

de memorie internă este de 55 Ko, dat de suma lungimilor componentei rezidente şi a celei mai mari ramuri.

Programul principal şi unităţile standard (30 KO) Unitatea C (5 KO) Unitatea A (10 KO) Unitatea B (20 KO)

Fig. 7.1 Exemplu de structură cu reacoperire Pentru construirea şi gestiunea structurilor cu reacoperire, mediul de dezvoltare Turbo Pascal are o componentă specializată (Overlay Manager), ale cărei proceduri şi funcţii sunt reunite în unitatea Overlay. Folosirea acestei componente presupune: a) Pe planul declaraţiilor: • Includerea unităţii Overlay în clauza USES a programului principal, cu obligaţia de a fi prima referită. • Introducerea directivelor {$O nume_unitate}, după clauza USES. Fiecare directivă nominalizează unitatea care va fi inclusă în structura de reacoperire (se generează instrucţiuni care vor include unitatea în fişierul Overlay). • Fiecare unitate care va fi inclusă în reacoperire se va compila cu directiva {$O+}. Prezenţa acesteia nu realizează includerea unităţii în reacoperire, ci doar semnalează compilatorului să asigure o structură a codului obiect, care să permită eventuala sa includere într-o astfel de structură. În concluzie, o aceeaşi versiune de unitate, compilată cu {$O+}, va putea fi folosită atât în structuri cu reacoperire, cât şi în structuri liniare. • Pentru asigurarea generării tuturor apelurilor după modelul FAR (cu adresă de segment şi cu deplasare), se va folosi directiva de compilare {$F+}, atât în programul principal, cât şi în unităţile care urmează a fi incluse în reacoperire. b) Pe planul instrucţiunilor executabile, folosirea tehnicii de reacoperire presupune apelul unor proceduri şi funcţii ale unităţii Overlay, în cadrul programului principal (în componenta rezidentă). • În primul rând este necesară iniţierea programului de gestiune a reacoperirii segmentelor, înaintea execuţiei altor instrucţiuni (inclusiv a celor de iniţializare din unităţi). Acest lucru se realizează cu procedura OvrInit, definită astfel: OvrInit(nume_fisier:STRING), unde nume_fisier este numele fişierului, cu extensia implicită .OVR, în care sistemul încarcă unităţile incluse în structura de reacoperire.

124

Tehnici speciale în Pascal

Pentru exemplul din figura 7.1, pot fi concepute, principial, structurile din figura 7.2. În cazul în care una dintre unităţile apelate conţine parte de iniţializare, datorită restricţiei ca procedura OvrInit să se execute prima, corelat cu faptul că partea de iniţializare a unei unităţi se lansează în execuţie cu prioritate, va trebui construită o unitate separată, care să conţină în partea de iniţializare apelul procedurii OvrInit şi care să fie invocată prima în clauza USES a programului principal (această unitate va conţine USES Overlay). {$O+,F+} UNIT A; Interface ……………. Implementation ……………. END.

{$O+,F+} {$O+,F+} (* Optional *) UNIT B; UNIT C; Interface Interface ……………. ……………. Implementation Implementation ……………. ……………. END. END. a) Unităţile {$O+,F+} PROGRAM PP; {Apelatorul} USES Overlay, A,B,C {$O A} {$O B} ………………. BEGIN OvrInit(‘ALFA.OVR’); ………………. END. b) Programul

Fig. 7.2 Exemplu de principiu de realizare a unei structuri cu reacoperire Pe parcursul execuţiei programului, folosirea tehnicii de reacoperire se traduce în încărcarea succesivă a unităţilor, pe măsura referirii lor, într-un buffer al structurii de reacoperire, căruia i se asociază memorie la începutul zonei heap. La apelul procedurii OvrInit, mărimea buffer-ului de reacoperire este setată implicit la mărimea celei mai mari unităţi incluse în reacoperire, la care se adaugă o zonă suplimentară pentru informaţii de interfaţă. Dimensiunea curentă a buffer-ului de reacoperire în număr de octeţi poate fi determinată, în orice moment pe parcursul execuţiei programului, prin apelul funcţiei OvrGetBuf, definită astfel: OvrGetBuf:Longint. Referitor la mecanismul de folosire a buffer-ului, trebuie menţionat că o unitate deja încărcată va fi ştearsă (suprascrisă), numai dacă nu există suficient spaţiu liber în restul buffer-ului pentru a încărca următoarea unitate. Dacă există spaţiu

125

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

suficient, noua unitate va fi încărcată, evitând reluarea încărcării unora dintre unităţi de fiecare dată când sunt referite. Dacă utilizatorul doreşte să mărească dimensiunea buffer-ului, tocmai pentru a rezolva asemenea probleme, se poate folosi procedura OvrSetBuf, definită astfel: OvrSetBuf(bufsize:Longint), unde bufsize precizează noua mărime a buffer-ului, care trebuie să fie mai mare decât cea implicită, fără a depăşi valoarea returnată de funcţia MemAvail: bufsize<=MemAvail+OvrGetBuf (MemAvail returnează volumul total de memorie nealocată în heap). Procedura OvrClearBuf eliberează buffer-ul, pentru utilizări în alte scopuri. Pentru a elimina repetarea operaţiilor de I/E necesare încărcării unităţilor din fişierul asociat structurii de reacoperire, este posibilă folosirea memoriei EMS (Expanded Memory System), prin apelul procedurii OvrInitEMS (fără parametri). În acest caz, se va asigura încărcarea fişierului în memoria EMS (dacă aceasta are spaţiu suficient), preluarea unităţilor în buffer-ul structurii de reacoperire realizân-du-se foarte rapid, direct din această memorie, fără operaţii de I/E. Dacă nu există spaţiu suficient, procedura este inefectivă. Execuţia procedurilor şi funcţiilor de gestiune a structurilor de reacoperire se poate încheia cu succes sau cu eroare, situaţie care poate fi determinată pe baza folosirii variabilei predefinite OvrResult. În unitatea Overlay sunt declarate mai multe constante simbolice asociate unor valori ale variabilei OvrResult (anexa 5).

7.2 Tehnica gestiunii proceselor În cele ce urmează, prin proces se desemnează un program executabil. În cazul unor sisteme de programe, utilizatorul are de ales între două variante de lansare în lucru a programelor componente: - lansare independentă, caz în care intercondiţionările dintre programe se rezolvă prin ordinea de execuţie a acestora. Întrucât procesele nu pot comunica între ele, eventuale informaţii privind modul lor de încheiere vor fi preluate şi prelucrate de sistemul de operare. - lansare din interiorul altor programe, stabilindu-se raporturi de tipul proces părinte - proces fiu. Pentru sistemele de programe se poate constitui, la limită, un singur proces părinte, cu rol de monitor, care lansează succesiv în execuţie procesele fiu. Programul monitor poate prelucra informaţiile referitoare la modul de încheiere a fiecărui proces fiu şi poate lua decizii privind modul de continuare. Unitatea Dos oferă posibilitatea implementării acestei soluţii prin intermediul unor proceduri şi funcţii specifice. • Lansarea în execuţie a unui proces fiu se poate realiza cu procedura al cărei antet este: Exec(specif,parametru:STRING). Procedura încarcă în memorie şi lansează în execuţie programul al cărui format executabil se găseşte în fişierul cu

126

Tehnici speciale în Pascal

specificatorul specif. Argumentul parametru conţine şirul de caractere corespunzând eventualilor parametri daţi prin linia de comandă la lansarea acestui program. Întrucât este necesară asigurarea de spaţiu în memorie şi pentru procesul fiu, în programul corespunzând procesului părinte trebuie să se reducă dimensiunea zonei heap, folosind directiva $M. • Salvarea vectorilor de întrerupere se realizează prin procedura SwapVectors. Procedura este destinată asigurării independenţei dintre procesul părinte şi un proces fiu în folosirea unor rutine de întrerupere instalate de către celălalt proces. Se apelează înainte şi după fiecare apel al procedurii Exec şi schimbă între ei vectorii de întrerupere curenţi cu pointerii SaveIntxx din unitatea System. • Preluarea codului de retur al unui proces fiu se poate realiza prin funcţia: DosExitCode:WORD. Codul de retur al procesului fiu poate fi poziţionat prin parametrul folosit la apelul rutinei Halt. Exemplu: 7.1. Se consideră un program director (Monitor) care lansează în execuţie trei programe, în cadrul unei aplicaţii multifunţionale. Cele trei programe sunt memorate în fişierele Fis1.EXE, Fis2.EXE, Fis3.EXE. PROGRAM Monitor; {$M 2048,0,0} USES CRT; VAR c:CHAR; BEGIN REPEAT ClrScr; WriteLn(’ Functiile realizate de program sunt:’); WriteLn(’ ’:5,’1 - Semnificatie functia 1’); WriteLn(’ ’:5,’2 - Semnificatie functia 2); WriteLn(’ ’:5,’3 - Semnificatie functia 3’); WriteLn(’ ’:5,’4 - STOP); Write (’Tastati functia dorita [1-4] : ’); Readln( c ); CASE c OF ’1’: Exec(’Fis1’,’’); ’2’: Exec(’Fis2’,’’); ’3’: Exec(’Fis3’,’’); ’4’: WriteLn(‘<< Program terminat>>’) ELSE WriteLn(’***Cod eronat de functie***’) END UNTIL c=’4’ END.

127

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

7.3 Lansarea programelor în execuţie prin linie de comandă În general, lansarea în execuţie a programelor Turbo Pascal este posibilă în două moduri: din interiorul mediului de programare, prin una din comenzile meniului Run; la nivelul sistemului de operare, printr-o linie de comandă. Prima variantă este utilă pe parcursul procesului de punere la punct a programelor, iar a doua este preponderentă în exploatarea curentă a unui produs-program. Pentru lansarea prin linie de comandă, utilizatorul are posibilitatea includerii, după numele fişierului .EXE, a unor parametri destinaţi programului. Linia de comandă are formatul general: ...>nume parametru [parametru]... Parametrii sunt şiruri de caractere, separate prin spaţii sau tab-uri, care sunt accesibili din programele Turbo Pascal prin două funcţii aparţinând unităţii System. Numărul de parametri din linia de comandă poate fi determinat folosind funcţia ParamCount, definită astfel: ParamCount:WORD. Accesul la un anumit parametru este posibil prin funcţia ParamStr, al cărei antet are forma: ParamStr(n:WORD):STRING. Funcţia returnează un şir de caractere care conţine al n-lea parametru de pe linia de comandă. Este posibil şi apelul de forma ParamStr(0), caz în care şirul returnat conţine calea şi numele fişierului .EXE asociat programului. Exemplu: 7.2. Se consideră programul ExDir, care poate prelua din linia de comandă directorul în care se găsesc fişierele aplicaţiei, transformându-l în director curent. Se asigură salvarea directorului în uz de la momentul lansării, respectiv restaurarea lui la încheierea execuţiei programului. Dacă noul director nu există, este prevăzută posibilitatea creării şi activării lui ca director curent. În cadrul programului, procedura OpDir asigură următoarele operaţii: determinarea directorului curent ('D'), schimbarea directorului curent ('S'), crearea unui nou director ('C'). Procedura returnează o variabilă de eroare (rez) când operaţiile asupra directoarelor nu se pot executa. PROGRAM ExDir; VAR r : INTEGER; c : CHAR; dinc,dnou: STRING; PROCEDURE OpDir(op:CHAR; VAR d:STRING; VAR rez:INTEGER); BEGIN CASE UpCase(op) OF 'D': GetDir(0,d); 'S': BEGIN {$I-} ChDir(d); {$I+} rez:=IOResult END; 'C': BEGIN {$I-} MkDir(d); {$I+} rez:=IOResult END END END; PROCEDURE DetDir;

128

Tehnici speciale în Pascal VAR dir: STRING; BEGIN OpDir('d',dir,r); WriteLn('Director curent: ',dir) END; BEGIN WriteLn('Program in executie curenta: ',ParamStr(0)); DetDir; IF ParamCount <> 0 THEN BEGIN dnou:=ParamStr(1); OpDir('d',dinc,r); OpDir('s',dnou,r); IF r <> 0 THEN BEGIN WriteLn('Director inexistent: ',ParamStr(1)); Write('Se creeaza ca director nou?(Y/N): '); ReadLn(c); IF UpCase(c) = 'Y' THEN BEGIN OpDir('c',dnou,r); IF r = 0 THEN OpDir('s',dnou,r) ELSE BEGIN WriteLn('Eroare la creare director. Executie intrerupta!' ); RunError(r) END END END END; DetDir END; {restul programului se scrie normal} IF ParamCount <> 0 THEN OpDir('s',dinc,r); DetDir END.

7.4 Tehnica tratării întreruperilor Microprocesorul gestionează un sistem de întreruperi. Datorită complexităţii proceselor ce se pot desfăşura în cadrul sistemului, unitatea centrală de prelucrare (UCP) nu poate asigura simultan un control complet, fiind focalizată la un moment dat pe o singură activitate. Pentru a suplini această lipsă, calculatorul este proiectat astfel încât, la apariţia oricărui eveniment ce presupune o anumită acţiune, să se genereze un semnal de întrerupere prin care este informată UCP. Aceasta suspendă temporar alte activităţi pentru a trata evenimentul apărut, transferând controlul la o rutină de tratare a întreruperii, care poate provoca afişarea unui mesaj de avertizare, reluarea sau încheierea execuţiei programului (normală, cu eroare sau forţată) etc. La încheierea execuţiei rutinei, UCP revine la activitatea iniţială. Microcalculatoarele IBM PC şi compatibile acceptă 256 de întreruperi, cu coduri din intervalul 0..255 (în hexa, $00..$FF). În literatura de specialitate, întreruperile se regăsesc clasificate în diverse moduri: • hardware, software; • ale

129

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

microprocesorului, hardware, software, DOS, Basic, de adresă, de uz general; • ale microprocesorului, BIOS, ale sistemului de operare (din care fac parte şi cele referitoare la funcţiile DOS) etc. Pentru asigurarea accesului la rutinele de întrerupere, prima zonă de 1 Ko din memoria internă conţine o tabelă de pointeri către fiecare din rutinele disponibile, dimensionată pentru a putea memora toate cele 256 de adrese teoretic posibile şi numită tabela vectorilor de întrerupere. Considerând o rutină cu codul i din intervalul 0..255, pointerul asociat ei în tabelă se află la adresa 4*i. Utilizatorul poate determina adresa unei rutine de întrerupere prin procedura GetIntVec definită în unitatea DOS astfel: GetIntVec(int:BYTE;VAR adrint:POINTER), unde int reprezintă codul întreruperii precizat în apelator, iar în adrint se returnează adresa rutinei de întrerupere corespunzătoare. Pentru ilustrarea utilizării acestei proceduri, se prezintă programul TabVectInt care afişează, în mai multe ecrane succesive, întreaga tabelă a vectorilor de întrerupere, cu precizarea codului şi a adresei rutinei asociate fiecărei întreruperi, cu componentele de segment şi deplasare, exprimate în hexazecimal (conversia se realizează de către funcţiile HexOctet şi HexCuvint incluse în unitatea Hex). PROGRAM TabVectInt; USES Dos,Crt,Hex; VAR i : BYTE; adrint: POINTER; PROCEDURE Defilare; VAR car: CHAR; BEGIN GotoXY(1,25); Write('Press any key ...'); car:=ReadKey; ClrScr END; BEGIN ClrScr; FOR i:=0 TO 255 DO BEGIN GetIntVec(i,adrint); IF (i+1) MOD 24 = 0 THEN Defilare; WriteLn(HexOctet(i):5,HexCuvant(Seg(adrint^)):5, HexCuvant(Ofs(adrint^)):5) END; Defilare END. UNIT Hex; INTERFACE FUNCTION HexOctet(nroct:BYTE):STRING; FUNCTION HexCuvint(nrcuv:WORD):STRING; IMPLEMENTATION FUNCTION CifraHex(cifra:BYTE):CHAR;

130

Tehnici speciale în Pascal BEGIN IF cifra < 10 THEN CifraHex:=Chr(48+cifra) ELSE CifraHex:=Chr(55+cifra) END; FUNCTION HexOctet; BEGIN HexOctet:=CifraHex(nroct DIV 16) + CifraHex(nroct MOD 16); END; FUNCTION HexCuvant; BEGIN HexCuvint:=HexOctet(Hi(nrcuv))+HexOctet(Lo(nrcuv)) END END.

Apelul rutinei de întrerupere se realizează prin procedura Intr, definită în unitatea System astfel: Intr(ni:BYTE; VAR reg:Register), unde ni este codul întreruperii, iar reg este o variabilă care specifică, într-o formă standard, diverşi parametri. Tipul Registers, definit în unitatea Dos (anexa 1), permite accesul la registrele unităţii centrale. Înainte de apel, trebuie pregătite anumite registre, în funcţie de parametrii solicitaţi de întrerupere. Astfel, în semiregistrul Reg.AH trebuie încărcat codul funcţiei, în Reg.AL codul subfuncţiei (când există) etc. Principalele caracteristici ale întreruperilor (folosindu-se ultima clasificare prezentată anterior) sunt: • întreruperile microprocesorului sunt generate de el însuşi: 0 corespunde situaţiei de împărţire la zero, 2 corespunde erorii de paritate în memorie, 1 şi 3 sunt folosite în depanarea programelor; 5 provoacă tipărirea conţinutului ecranului pe imprimantă etc. Alte întreruperi sunt generate de diverse componente hardware din configuraţia sistemului. • rutinele BIOS sunt realizate de către producătorul echipamentului şi memorate în ROM. La începutul unei sesiuni de lucru cu calculatorul, adresele din ROM ale rutinelor BIOS sunt încărcate în tabela vectorilor de întrerupere. Când un program cheamă o rutină BIOS, Turbo Pascal preia adresa acesteia din tabelă şi o execută. Exemple de întreruperi BIOS: $09 - apăsarea unei taste; $10 - servicii video; $12 - determinarea dimensiunii memoriei existente în sistem; $13 - gestiunea discurilor; $15 - determinarea unor caracteristici ale memoriei etc. În programele Int15 şi Int12 sunt prezentate exemple de activare a întreruperilor $15 şi $12. Pentru întreruperea $15, se apelează funcţia $88, care returnează în registrul AX dimensiunea memoriei extinse exprimată în Ko. Întreruperea $12 nu foloseşte registrul AH. PROGRAM Int15; USES Dos; FUNCTION ExtendedMemory:WORD; VAR reg: Registers; BEGIN reg.AH:=$88; Intr($15,Reg); ExtendedMemory:=reg.AX END; BEGIN WriteLn('Memorie extinsa= ', ExtendedMemory,' Ko') END.

PROGRAM Int12; USES Dos; FUNCTION MemorySize: WORD; VAR reg: Registers; BEGIN Intr($15,Reg); MemorySize:=reg.AX END; BEGIN WriteLn('Memorie interna= ', MemorySize,' Ko') END.

131

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

• rutinele de întrerupere ale sistemului de operare sunt memorate pe disc şi sunt încărcate în memoria internă la începutul fiecărei sesiuni de lucru. Acestea corespund întreruperilor software. Exemple de întreruperi SO: $20 - terminare program; $21 - funcţii DOS; $22 - furnizarea adresei de terminare; $24 - rutina de tratare a erorilor critice. Una dintre cele mai utilizate întreruperi este $21 (rutine DOS). Adresele rutinelor, corespunzând diferitelor servicii DOS, sunt memorate în tabela vectorilor de întrerupere, în poziţiile neocupate de rutinele BIOS. Exemple de funcţii DOS: $00 - terminare program; $01 - citire cu ecou de la tastatură; $08 - citire fără ecou de la tastatură; $25 - setarea vectorului de întrerupere; $2B - setarea datei din sistem; $2C - citirea datei din sistem; $2D setarea orei din sistem etc. Apelul unei rutine DOS se poate realiza cu Intr($21,Reg) sau cu o procedură specială definită în unitatea DOS astfel: MsDos(VAR Reg:Registers). Întrucât toate rutinele corespund unor funcţii, este obligatorie încărcarea prealabilă a codului acestora în semiregistrul AH, înaintea apelului procedurii MsDos sau Intr. Aproape toate cele 80 de funcţii DOS se regăsesc în cele peste 200 de proceduri şi funcţii ale unităţilor standard din Turbo Pascal. Analizând evantaiul posibilităţilor ce se deschid programatorului care foloseşte Turbo Pascal, la extreme se constată următoarele variante de abordare: folosirea exclusivă a procedurilor şi funcţiilor din unităţile standard ale mediului Turbo Pascal (care apelează, la rândul lor, funcţiile DOS); folosirea directă a rutinelor de întrerupere asociate funcţiilor DOS prin apeluri adecvate ale procedurilor MsDos (sau Intr). Prima soluţie este mai simplu de utilizat în scrierea programului sursă, dar cea de-a doua conduce la creşterea vitezei de execuţie a programelor. În concluzie, se poate reţine recomandarea ca a doua soluţie, care presupune cunoaşterea a o serie de detalii tehnice, să fie folosită doar atunci când este importantă asigurarea unui timp de răspuns cât mai scurt. Evident, la limită, este posibilă "ocolirea" integrală a procedurilor şi funcţiilor din bibliotecile Turbo Pascal, dar nici una din variante nu poate fi absolutizată. Pentru analiza comparată a celor două variante se ilustrează folosirea lor pe exemplul funcţiilor DOS de acces la data din sistem. Accesul din Turbo Pascal se poate realiza cu procedurile GetDate, pentru preluarea datei curente, respectiv SetDate, pentru modificarea acesteia. Procedura GetDate are următorii parametri, pentru care se precizează şi limitele domeniului de valori admis: anul (1980..2099), luna (1..12), ziua în cadrul lunii (1..31), ziua în cadrul săptămânii (0..6, cu duminică=0, luni=1, ..., sâmbătă=6). Procedura SetDate foloseşte numai primii trei parametri. În programul Calendar_1 se ilustrează apelul acestor proceduri pentru determinarea calendarului unei luni oarecare, din intervalul de ani acceptat (1980..2099). PROGRAM Calendar_1; USES Dos,Crt; VAR ac,lc,zc,zsc: WORD;

132

Tehnici speciale în Pascal an,ln,zn,zsn: WORD; i,nrz: BYTE; BEGIN REPEAT Write('Luna, an: '); ReadLn(ln,an); UNTIL (an > 1979) AND (an < 2100) AND (ln IN [1..12]); zn:=1; GetDate(ac,lc,zc,zsc); SetDate(an,ln,zn); GetDate(an,ln,zn,zsn); SetDate(ac,lc,zc); CASE ln OF 1,3,5,7,8,10,12: nrz:=31; 4,6,9,11 : nrz:=30; 2 : IF an MOD 4 =0 THEN nrz:=29 ELSE nrz:=28 END; ClrScr; WriteLn(' ',ln:2,' - ',an:4,#13#10); WriteLn(' L M M J V S D'); WriteLn(' ---------------------'); IF zsn = 0 THEN zsn:=7; FOR i:=1 TO nrz+zsn-1 DO BEGIN IF i <= zsn-1 THEN Write(' ') ELSE Write(i-zsn+1:3); IF i MOD 7 = 0 THEN WriteLn END; WriteLn; WriteLn(' ---------------------') END.

Primul apel al procedurii GetDate asigură salvarea datei curente (în variabilele ac, lc, zc, respectiv zsc), care se reface prin al doilea apel al procedurii SetDate. Algoritmul folosit se bazează pe faptul că, intern, se asigură determinarea zilei din săptămână asociată oricărei date din intervalul de ani acceptat. Astfel, după solicitarea lunii (ln) şi a anului (an) pentru care se cere afişarea calendarului, se va determina ziua din săptămână (zsn) pentru prima zi a lunii, urmată de toate celelalte. Rutinele de întrerupere DOS corespunzătoare celor două proceduri au codurile $2A, respectiv $2B. După apelul rutinei $2A, rezultatele sunt furnizate în următoarele (semi)registre: anul în CX, luna în DH, ziua în cadrul lunii în DL, iar ziua în cadrul săptămânii în AL. Corespunzător, apelul rutinei $2B trebuie precedat de încărcarea valorilor asociate noii date din sistem în CX, DH şi DL. Folosirea acestei soluţii presupune: adăugarea unei declaraţii de forma: reg1,reg2: Registers; înlocuirea celor patru instrucţiuni de apel al procedurilor GetDate şi SetDate cu următoarea secvenţă (restul programului rămânând nemodificat): reg1.AH:=$2A; MsDos(reg1); ac:=reg1.CX; lc:=reg1.DH; zc:=reg1.DL; zsc:=reg1.AL; reg2.AH:=$2B; reg2.CX:=an; reg2.DH:=ln; reg2.DL:=zn; MsDos(reg2);

reg1.AH:=$2A; MsDos(reg1); zsn:=reg1.AL; reg2.AH:=$2B; reg2.CX:=ac; reg2.DH:=lc; reg2.DL:=zc; MsDos(reg2);

133

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

7.5 Tehnici de lucru la încheierea execuţiei programelor Încheierea execuţiei unui program este un eveniment important, care trebuie tratat de programator cu toată atenţia. Turbo Pascal asigură utilizatorului posibilitatea implicării directe, atât în declanşarea acestui eveniment, cât şi în realizarea unor prelucrări suplimentare asociate lui. Terminarea unui program poate fi de mai multe categorii: normală, cu eroare la execuţie, forţată. În primele două cazuri, decizia de terminare a execuţiei aparţine sistemului, iar în ultimul terminarea se execută înaintea atingerii sfârşitului logic al programului, la solicitarea expresă a utilizatorului. Variabilele ExitCode şi ErrorAddr, definite în unitatea System, conţin informaţii privind modul de încheiere a execuţiei. La încheierea cu eroare la execuţie, ExitCode va conţine numărul erorii, iar ErrorAddr punctează pe codul asociat instrucţiunii care a generat această eroare. La lansarea în execuţie a unui program, cele două variabile au valorile iniţiale 0, respectiv nil. La încheiere normală, situaţia celor două variabile este ExitCode=0 şi ErrorAddr=nil. Mediul Turbo Pascal determină dacă este necesară afişarea unui mesaj de eroare la execuţie, pe baza valorii variabilei pointer ErrorAddr. Mesajul de eroare se afişează numai dacă ErrorAddr ≠ nil. Teoretic, încheierea forţată a execuţiei, înaintea atingerii sfârşitului logic al programului, contravine principiilor programării structurate. Totuşi, practica programării conduce la situaţii când o asemenea încheiere devine necesară, dar se recomandă limitarea folosirii ei numai la cazurile când nu sunt posibile şi alte soluţii. • Procedura de încheiere a execuţiei blocului curent are antetul de forma: Exit. În cazul subprogramelor, apelul procedurii provoacă revenirea în apelator (blocul din care a fost apelat subprogramul), iar pentru programe determină încheierea execuţiei. • Procedura de încheiere a execuţiei cu un cod de retur precizat de utilizator este definită astfel: Halt[(ExitCode:WORD)]. La apelul procedurii cu parametru, ExitCode va primi valoarea transferată la execuţia apelului, iar în cazul apelului fără parametru rămâne pe valoarea 0; în ambele cazuri, ErrorAddr rămâne cu valoarea nil. Prin apelul procedurii (care poate fi plasat chiar la sfârşitul logic al programului, fără a încălca principiile programării structurate), se poate face distincţia între mai multe moduri de terminare normală a unui program. Exemplu: 7.3. Fiind dat un vector de mari dimensiuni, memorat într-un fişier binar, să se realizeze programul de ventilare a fişierului cu obţinerea a două fişiere binare, cu valorile pozitive, respectiv negative ale fişierului (valorile nule vor fi neglijate). Programul Ventilare realizează încheierea execuţiei cu apelul procedurii Halt, care,

134

Tehnici speciale în Pascal

prin poziţionarea codului de retur, permite distincţia între diversele moduri de încheiere a execuţiei: ambele fişiere nevide, ambele fişiere vide, respectiv unul sau altul dintre fişierele de ieşire vid. PROGRAM Ventilare; VAR fi,fp,fn: FILE OF INTEGER; v : INTEGER; np,nn:WORD; BEGIN Assign(fi,'FI.DAT'); Assign(fp,'FP.DAT'); Assign(fn,'FN.DAT'); Reset(fi); Rewrite(fp); Rewrite(fn); WHILE NOT Eof(fi) DO BEGIN Read(fi,v); IF v > 0 THEN Write(fp,v) ELSE IF v < 0 THEN Write(fn,v) END; np:=FileSize(fp); nn:=FileSize(fn); IF (np <> 0) AND (nn <> 0) THEN Halt {0 - ambele fisiere nevide} ELSE IF np <> 0 THEN Halt(1) {1 - fisierul FN vid} ELSE IF nn <> 0 THEN Halt(2) {2 - fisierul FP vid} ELSE Halt(3) {3 - ambele fisiere vide} END.

La revenirea în contextul de lansare a programului în execuţie, există soluţii, oferite de mediul Turbo Pascal (prin variabila DosExitCode) sau de sistemul de operare DOS, pentru testarea valorii codului de retur şi luarea unor decizii adecvate de continuare a prelucrărilor. În programul GestProc se creează fişierul binar iniţial, se lansează în execuţie programul Ventilare (considerat memorat în fişierul VENT.EXE) şi se prelucrează codul de retur. PROGRAM GestProc; USES Dos; {$M 2048,0,0} VAR fi : FILE OF INTEGER; vi,n,i: INTEGER; r : WORD; BEGIN Write('Numar de elemente si valoare initiala:'); ReadLn(vi,n); Assign(fi,'FI.DAT'); Rewrite(fi); FOR i:=vi TO vi+n-1 DO Write(fi,i); Close(fi); SwapVectors; Exec('VENT',''); SwapVectors; r:=DosExitCode; IF r = 0 THEN WriteLn('OK!') ELSE IF r = 1 THEN

135

Programarea calculatoarelor – Tehnica programării în limbajul Pascal WriteLn('Fisierul FN vid') ELSE IF r = 2 THEN WriteLn('Fisierul FP vid') ELSE WriteLn('Ambele fisiere vide') END.

• Procedura de încheiere cu eroare de execuţie este definită astfel: RunError[(ExitCode:WORD)]. La apelul fără parametru, ExitCode rămâne cu valoarea 0, iar în cazul apelului cu parametru valoarea parametrului real va fi atribuită variabilei ExitCode.

Exemplu: 7.4. În programul Valid se realizează introducerea cu validare a unei valori numerice, cu reluare până la furnizarea unei valori corecte, respectiv până la depăşirea unui număr limită de reluări. PROGRAM Valid; VAR n,i,er : BYTE; x : REAL; BEGIN Write('Numar maxim admis de reluari:'); ReadLn(n); i:=0; REPEAT er:=0; Write('x:'); {$I-} ReadLn(x); {$I+} IF IOResult <> 0 THEN BEGIN Write('Valoare nenumerica'); er:=1 END; Inc(i) UNTIL (er = 0) OR (i > n); IF i > n THEN BEGIN Write('Depasire numar de reluari admis'); RunError(106); END END.

Soluţia propusă conduce la întreruperea forţată prin apelul RunError, la depăşirea numărului de repetări admis, cu mesajul standard de eroare corespunzător codului dat de utilizator: Runtime error 106: Invalid numeric format • Includerea rutinelor proprii de încheiere a execuţiei programelor. Pe lângă operaţiile realizate de sistemul de operare MS-DOS, Turbo Pascal este prevăzut cu câteva rutine de serviciu, a căror lansare, într-o anumită succesiune, este declanşată implicit la terminarea execuţiei unui program. La apariţia evenimentului de terminare a unui program, controlul este transferat primei proceduri din lanţul rutinelor de încheiere a execuţiei (exit procedures), a cărei adresă este memorată în variabila ExitProc. Utilizatorul are posibilitatea să includă propria rutină de tratare a terminării, al cărei apel se va insera la începutul lanţului de apeluri. În acest scop, variabila

136

Tehnici speciale în Pascal

ExitProc va fi setată pe adresa procedurii proprii, cu asigurarea refacerii valorii iniţiale după încheierea execuţiei acestei proceduri, asigurându-se astfel continuarea cu procedurile implicite de încheiere. Procedurile de încheiere proprii oferă programatorului un mijloc de a controla - nu şi de a preveni - terminarea unui program. De exemplu, dacă programul foloseşte o imprimantă, în acest mod se poate asigura sesizarea momentului pentru extragerea ultimei pagini. Tehnica de includere în lanţ a procedurii proprii este ilustrată în programul Ex_ExitProgram. PROGRAM Ex_ExitProgram; VAR ExitOrig: POINTER; {F+} PROCEDURE ExitPropriu; {$F-} BEGIN {Instructiuni ale utilizatorului la terminarea programului } WriteLn('Terminarea programului...'); ExitProc:= ExitOrig; {restaurarea adresei rutinei implicite de incheiere a executiei} END; BEGIN ExitOrig:=ExitProc; {salvarea adresei rutinei implicite de incheiere a executiei} ExitProc:=@ExitPropriu; {includerea rutinei proprii de incheiere la inceputul lantului de apeluri} {Restul programului se scrie normal} END.

Deşi procedura ExitPropriu nu este chemată explicit, ea va fi prima procedură apelată la terminarea execuţiei programului, efectul vizibil fiind afişarea mesajului inclus în cadrul ei. Se remarcă folosirea directivei {$F}, necesară pentru a forţa tratarea procedurii ExitPropriu ca "îndepărtată" (far). Acest lucru este necesar deoarece rutinele de încheiere implicite aparţin segmentului de cod al unităţii System, în timp ce procedura ExitPropriu este inclusă în segmentul de cod al programului principal.

7.6 Punerea la punct a programelor Turbo Pascal În raport de momentul în care se manifestă, pot fi identificate trei categorii de erori: de compilare, la execuţie şi de logică. Erorile de compilare (Compile-Time Errors) sunt, în general, rezultatul unor greşeli lexicale sau sintactice. Erorile lexicale apar când un identificator, un literal sau un simbol folosit în program este ilegal. De exemplu, considerând declaraţia: CONST hex1=$FFHH;

137

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

se produce eroare lexicală, deoarece în specificarea valorii constantei hexa apar caractere nepermise (HH). Eroarea este semnalată printr-un mesaj de forma: Error 7: Error in integer constant. O eroare similară ar produce şi folosirea unui literal hexa $-12, deoarece aceşti literali nu pot avea semn. Declaraţia de constantă simbolică: CONST sir='pret unitar=; generează eroare lexicală, compilatorul interpretând absenţa apostrofului de închidere a şirului, în sensul următorului mesaj: Error 8: String constant exceeds line. Erorile sintactice corespund cazurilor în care unele construcţii folosite în program încalcă regulile formale ale limbajului, cum ar fi specificarea unui număr greşit sau de tipuri neadecvate de parametri în apelul unor proceduri sau funcţii, omiterea caracterului ";" la încheierea unei instrucţiuni sau folosirea lui într-un context nepermis etc. Mesajele nu sunt întotdeauna foarte explicite, dar deplasarea cursorului în fereastra Turbo Pascal, exact în locul în care a fost sesizată eroarea, îl va ajuta pe utilizator să determine cauza acesteia. În plus, la folosirea tastei F1 se afişează o fereastră de informare cu explicaţii suplimentare privind semnificaţia erorii. De semnalat faptul că unele erori, care la o primă analiză par lexicale, sunt interpretate de compilator ca erori de sintaxă. Se consideră declaraţia: VAR pret-unitar: REAL; În sens strict, s-a produs o eroare lexicală, identificatorul fiind construit incorect ("-" în loc de "_"). Totuşi, mesajul de eroare al compilatorului este Error 86: ":" expected şi nici cel suplimentar, afişat la apăsarea tastei F1, A colon doesn't appear where it should nu este cu mult mai explicit. Compilatorul interpretează că identificatorul este pret, după care, fiind în secţiunea VAR a unui program, aşteaptă caracterul ":", care ar trebui să urmeze în sintaxa declaraţiei. Deoarece cursorul se plasează sub caracterul "-", este de presupus că utilizatorul va sesiza uşor adevărata cauză a erorii. În multe cazuri, eroarea sintactică este sesizată după locul strict în care s-a produs, în momentul în care compilatorul constată absenţa unui element pe care îl aştepta în contextul respectiv. De exemplu, considerând secvenţa: VAR pret_unitar: REAL BEGIN absenţa caracterului ";" de încheiere a declaraţiei variabilei se sesizează doar la începutul liniei următoare, mesajul de eroare fiind: Error 85: ";" expected, iar cursorul se va plasa la începutul cuvântului BEGIN. Pentru o instrucţiune de forma: IF a > b THEN WriteLn(a,' > ',b); ELSE WriteLn(a,' <= ',b); mesajul de eroare este: Error 113: Error in statement. Cauza erorii este folosirea incorectă a terminatorului ";" la încheierea ramurii THEN a instrucţiunii IF, iar eroarea este sesizată la întâlnirea lui ELSE, cu plasarea cursorului la începutul

138

Tehnici speciale în Pascal

acestuia. La folosirea tastei F1 se afişează fereastra de informare cu mesajul This symbol can't start a statement, care, de această dată, este suficient de explicit. Erorile la execuţie (Run-Time Errors) se mai numesc şi semantice şi apar atunci când instrucţiuni valide pe plan sintactic sunt combinate incorect. Printre cazurile mai frecvente din această categorie pot fi menţionate: încercarea de a realiza operaţii I/O cu fişiere nedeschise, tentativa de citire dincolo de sfârşitul unui fişier, depăşirea domeniului de valori permis pentru unele tipuri de variabile, depăşirea stivei, împărţire la zero etc. În toate cazurile, execuţia programului se încheie anormal, se afişează un mesaj de eroare, iar cursorul se deplasează în fereastra de editare a mediului Turbo Pascal, în contextul din care s-a generat eroarea, aşteptându-se intervenţia utilizatorului. Trebuie menţionat că, depinzând de modul de folosire a unor directive de compilare, programatorul poate inhiba sau nu semnalarea unora dintre erori sau poate prelua controlul în caz de eroare, evitând încheierea anormală şi prematură a programului. Un exemplu tipic din prima categorie este directiva {$R}, pentru controlul depăşirii domeniului de valori al datelor de tip întreg. Întrucât valoarea ei implicită este {$R-}, execuţia programului: VAR a: BYTE; BEGIN a:=255; a:=a+1; END. se va încheiea fără eroare, deşi la a doua operaţie de atribuire se depăseşte domeniul de valori admis pentru tipul BYTE. Introducând în program o primă linie cu directiva {$R+}, execuţia se încheie cu eroare, afişându-se mesajul Error 201: Range check error, iar cursorul se va plasa la începutul instrucţiunii care a generat eroarea. Un exemplu pentru evitarea încheierii anormale îl poate constitui directiva {$I}, care permite programatorului preluarea controlului în caz de eroare la realizarea operaţiilor de intrare/ieşire (programul Valid). Mesajele erorilor de execuţie au forma: Runtime error 106 at ssss:dddd, unde ssss reprezintă segmentul, iar dddd deplasarea corespunzând adresei instrucţiunii la care s-a produs eroarea. Execuţia se întrerupe, iar cursorul se va deplasa pe linia din programul sursă cu instrucţiunea care a generat eroarea (în exemplul considerat, linia cu instrucţiunea de apel al procedurii ReadLn). Erorile de logică (Logic Errors) sunt cel mai greu de îndepărtat, deoarece programul se poate încheia "normal", dar rezultatele nu sunt cele aşteptate. Acestea sunt generate de greşelile de proiectare a programelor. Pentru depistarea lor este necesară, de multe ori, folosirea unor tehnici speciale de depanare. Depanarea interactivă se realizează în cadrul unei sesiuni de depanare, care presupune execuţia supravegheată a programului. Utilizatorul are posibilitatea să aleagă între: folosirea unui depanator integrat (Integrated Debugger), inclus în mediul de programare; folosirea unui depanator autonom (Standalone Debugger), respectiv pachetul de programe Turbo Debugger al firmei Borland. Folosirea depanatorului integrat, abordată în cele ce urmează, presupune setarea comutatorului Options|Debugger|Integrated. În procesul de depanare

139

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

interactivă pot fi identificate două etape: pregătirea sesiunii de depanare, respectiv desfăşurarea acesteia. • Pregătirea sesiunii de depanare interactivă corespunde operaţiilor ce se desfăşoară pe parcursul fazelor de compilare şi editare de legături şi care, prin informaţiile produse, condiţionează desfăşurarea sesiunii propriu-zise. Informaţiile necesare procesului de depanare sunt generate de directive de compilare sau de comenzi echivalente din meniul Options. Folosirea depanatorului integrat depinde de modul de specificare a directivelor de compilare {$D} şi {$L}, cărora le corespund comutatoarele Options|Compile|Debug Information, respectiv Options|Compile|Local Symbols. Directiva {$D} activează sau inhibă generarea unor informaţii de depanare, care constau dintr-o tabelă ce precizează corespondenţa dintre numerele liniilor din programul sursă ce conţin instrucţiuni executabile şi adresa codului obiect generat pentru fiecare dintre ele. Valoarea implicită a acestei directive este {$D+} şi folosirea ei permite execuţia pas cu pas şi definirea punctelor de întrerupere în programele compilate în acest mod. În plus, la apariţia unor erori la execuţie, comanda Compile|Find Error asigură localizarea automată a instrucţiunii ce a produs eroarea. Directiva condiţionează şi obţinerea aşa numitei "mape" a editorului de legături, respectiv un fişier cu extensia .MAP, ale cărui caracteristici şi conţinut depind de modul de setare a butoanelor radio din grupul Options|Linker|Map File: (.) Off ( ) Segments ( ) Public ( ) Detailed Butonul Off este selectat implicit şi are ca efect neproducerea fişierului .MAP. La selectarea butonului Segments, fişierul va conţine informaţii privind adresa de start, lungimea şi numele segmentelor de cod, date şi stivă, precum şi adresa de început a zonei heap. Alegerea butonului Public face ca, pe lângă informaţiile privind segmentele, în fişierul .MAP să se includă un tabel cu adresele obiectelor "publice", adică ale subprogramelor interne, ale constantelor cu tip şi ale variabilelor globale definite în program, precum şi ale variabilelor globale ale unităţilor folosite (în absenţa clauzei USES, numai cele ale unităţii System). De asemenea, se precizează adresa punctului de lansare în execuţie a programului (program entry point). Cu butonul Detailed se afişează, în plus, un tabel cu numerele liniilor cu instrucţiuni executabile din programul sursă, urmate de adresa codului obiect generat în cadrul segmentului de cod al programului principal. Sunt incluse şi instrucţiunile executabile ale eventualelor subprograme interne. Directiva {$L} activează sau inhibă generarea unor informaţii despre simbolurile locale, adică variabilele şi constantele declarate în cadrul subprogramelor interne ale unui program. Valoarea implicită a directivei este {$L+} şi folosirea ei condiţionează examinarea şi modificarea variabilelor locale, precum şi urmărirea succesiunii de generare şi execuţie a apelurilor de proceduri şi funcţii interne. Directivele {$D} şi {$L} se folosesc de obicei împreună, cu precizarea că {$L} este ignorată dacă {$D} nu este activă. Pentru depanare, pentru a nu fi

140

Tehnici speciale în Pascal

condiţionaţi de eventualele setări implicite pe parcursul sesiunii de lucru, se recomandă includerea la începutul programului a configuraţiei: {$D+,L+}. Deoarece atât {$D+} cât şi {$L+} generează spaţiu de memorie suplimentar, după punerea la punct a programelor se recomandă resetarea acestora. • Desfăşurarea sesiunii de depanare interactivă. Depanatorul integrat oferă utilizatorului mai multe comenzi incluse în meniurile Run, Debug şi Window ale mediului Turbo Pascal, utile în procesul de depanare: execuţia pas cu pas a programelor, cu sau fără execuţia în aceeaşi manieră a subprogramelor; vizualizarea valorilor unor variabile (mai general, expresii), cu eventuala modificare a acestora; crearea unor puncte de întrerupere; vizualizarea conţinutului stivei. În practica programării, comenzile se grupează pe parcursul sesiunii de depanare în funcţie de obiectivele urmărite. Sesiunea de depanare este formată din următoarele componente: deschiderea sesiunii; sesiunea propriu-zisă; închiderea sesiunii. Chiar dacă tehnicile de depanare sunt diferite, în cadrul lor se regăsesc cel puţin două elemente comune: execuţia pas cu pas a instrucţiunilor, urmărirea în paralel a rezultatelor obţinute.

a) Deschiderea sesiunii se realizează cu una din comenzile meniului Run: - Run|Trace Into (F7) sau Run|Step Over (F8), când se urmăreşte depanarea de la începutul programului; - Run|Go to Cursor (F4), în cazul unei depanări dintr-un anumit punct. În primul caz, efectul comenzii este afişarea unei bare de execuţie (run bar), care supraluminează linia BEGIN a programului principal. În al doilea caz, anterior deschiderii sesiunii are loc deplasarea cursorului pe o anumită linie, iar la folosirea comenzii se execută instrucţiunile în mod normal pînă la linia marcată, după care se intră într-o manieră de lucru asemănătoare variantei anterioare. O variantă a depanării dintr-un anumit punct o constituie crearea punctelor de întrerupere (breakpoints), cu ajutorul cărora se selectează instrucţiunile din program corespunzând locurilor unor prezumtive greşeli de logică. Crearea punctelor de întrerupere se realizează anterior deschiderii sesiunii de depanare propriu-zise, prin deplasarea cursorului în textul sursă în contextul respectiv şi folosirea comenzii Debug|Toggle breakpoint (sau ). Punctul de întrerupere va fi marcat printr-o bară luminoasă, iar în timpul sesiunii de depanare execuţia programului se va întrerupe înainte de executarea instrucţiunilor de pe linia marcată. Comanda poate realiza şi eliminarea punctelor de întrerupere, dacă este folosită pentru linii unde există deja definite asemenea puncte. Punctele de întrerupere pot fi gestionate şi cu comanda Debug|Breakpoints, care, prin intermediul unei ferestre de dialog, permite ştergerea unor puncte (butonul de comandă Delete), definirea unor noi puncte de întrerupere sau modificarea caracteristicilor celor existente (butonul de comandă Edit). b) Sesiunea propriu-zisă corespunde aplicării unor tehnici de depanare, bazate pe folosirea într-o anumită succesiune a unor comenzi din meniurile Debug şi Window, după deschiderea acesteia cu una din comenzile menţionate ale meniului

141

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Run. Urmărirea valorilor variabilelor este asociată cu execuţia pas cu pas, de la început sau dintr-un anumit punct, fiind posibilă urmărirea modului cum evoluează valorile unor variabile (mai general, expresii) pe măsura execuţiei instrucţiunilor. În consecinţă, fie de la început, fie pe parcursul sesiunii, este necesară precizarea variabilelor (expresiilor) ale căror valori vor fi afişate într-o fereastră de urmărire (watch window). Se folosesc comenzile din submeniul Debug|Watch, prin care se poate specifica: includerea unui nou element (Add watch); ştergerea unui element (Delete watch); modificarea unui element (Edit Watch). Submeniul are o fereastră proprie de dialog cu un câmp de intrare în care se realizează operaţii asupra elementului curent din lista expresiilor de urmărit, marcat printr-o bară luminoasă. La prima introducere a unui element în listă are loc automat deschiderea ferestrei de urmărire şi amplasarea acestuia la începutul listei, cu afişarea valorii sale (la începutul sesiunii valoarea este nedefinită). Desfăşurarea sesiunii presupune execuţia pas cu pas a instrucţiunilor, de la început sau din punctul ales, cu afişarea valorilor variabilelor (expresiilor) incluse în fereastra de urmărire. Activarea ferestrei de urmărire, atât pentru urmărirea prin defilare a tuturor valorilor cât şi pentru operaţii de editare asupra listei, se realizează cu comanda Window|Watch. Urmărirea conţinutului stivei este legată de lucrul cu subprograme proprii, permiţând controlul succesiunii apelurilor de proceduri şi funcţii, până la atingerea apelului procedurii aflată curent în execuţie. Se bazează pe folosirea comenzii Window|Call stack (), care deschide o fereastră pentru afişarea succesiunii apelurilor de subprograme. c) Închiderea sesiunii se realizează cu comanda Run|Program reset ().

142

PRELUCRAREA ETICHETELOR DE FIŞIER ŞI A ALTOR INFORMAŢII EXTERNE MEMORATE ÎN DISCURI

Sistemul de operare gestionează o structură arborescentă de [sub]directoare şi fişiere. La fiecare nivel din arborescenţă există tabele care încorporează informaţii de legătură şi identificare a ascendenţilor şi descendenţilor. În suportul extern, un fişier are, pe lângă partea de date, o intrare în [sub]directorul părinte, numită în continuare şi etichetă. Aceasta memorează caracteristicile externe ale fişierului, cum ar fi: numele extern, extensia, atributele, data şi ora ultimei scrieri în fişier etc. Programele utilizatorului pot avea acces la unele informaţii memorate în etichetele fişierelor şi în alte tabele din structura DOS a discurilor, prin funcţii şi proceduri adecvate.

8.1 Structura fizică a discurilor DOS Un disc magnetic, în structură DOS, este compus din următoarele părţi: tabele de alocare a fişierelor, tabela directorului rădăcină, zona de date a fişierelor. În plus, discul sistem mai conţine zona programului de încărcare a sistemului de operare (BOOT), localizată pe primul sector al primei piste. Fiecare fişier (subdirector) are o intrare (tip etichetă) în directorul (subdirectorul) părinte. ♦ Tabela de alocare a fişierelor (FAT) este împărţită în câmpuri care corespund cluster-elor de pe disc. Cluster-ul este format din unul sau mai multe sectoare şi reprezintă unitatea de alocare pe disc. Numărul de sectoare ale unui cluster este memorat în blocul de parametri ai BIOS şi diferă de la un tip de disc la altul (flexibil sau Winchester) şi de la un tip de calculator la altul. Oricum, el este o putere a lui doi. Exemplu: 8.1. Disc flexibil simplă faţă - 20 sectoare/cluster; disc flexibil dublă faţă - 21 sectoare/cluster; disc Winchester pentru PC/AT - 22 sectoare/cluster; disc Winchester pentru PC/XT - 23 sectoare/cluster.

143

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

Câmpurile din FAT au 16 biţi (un cuvânt), prin care se pot adresa 216 clustere (la unele sisteme, câmpurile din FAT au 12 biţi). Primul cuvânt din FAT (cu numărul zero) conţine identificatorul tabelei de alocare, numit şi descriptorul suportului. El indică, codificat, tipul de disc utilizat. Al doilea cuvânt din FAT (cu numărul unu) conţine FFFF16. Începând de la cuvântul cu numărul doi, sunt memorate lanţuri de alocare pentru fiecare fişier. Un cuvânt din FAT poate avea următorul conţinut: - cluster-ul corespunzător este disponibil; 000016 FFF016-FFF616 - cluster-ul corespunzător este rezervat; FFF716 - cluster-ul corespunzător este defect; FFF816-FFFF16 - sfârşitul lanţului de alocare; 000216-FFEF16 - adresa următorului cuvânt din FAT, corespunzător următorului cluster ocupat de datele fişierului. Lanţul de legături este realizat prin aceea că fiecare cuvânt indică numărul următorului cuvânt din lanţ. Fişierul nu ocupă o zonă continuă în disc. El are cel puţin un cluster (când are un singur cluster, cuvântul corespunzător din FAT este FFFF16). Exemplu: 8.2. În figura 8.1 se prezintă o tabelă de alocare şi lanţul de legături realizat pentru un fişier, Fis.

Fig. 8.1 Exemplu de tabelă de alocare Pentru fişierul Fis sunt alocate cluster-ele 09-0C, 14-16, 18-19. Cluster-ul 17 este defect. Cluster-ele 0D-13, 1A-1F sunt disponibile. Un alt lanţ începe cu cluster-ul 02 şi se termină cu cluster-ul 07. Prin algoritmi corespunzători, MS-DOS face conversia de la adresă de cluster la adresă de sector. Pentru a evita pierderea informaţiilor, în cazul deteriorării unor câmpuri, în disc sunt păstrate două exemplare (adiacente) din FAT. ♦ Zona tabelei directorului rădăcină (DIR) urmează, în suport, tabelei de alocare a fişierelor şi conţine câte o intrare pentru fiecare fişier sau subdirector descendent. Intrarea este o zonă de 32 octeţi cu o structură prestabilită. Fiecare disc are

144

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

un singur director rădăcină, căruia îi este asociată zona DIR. Dimensiunea şi poziţia tabelei DIR sunt fixe, prestabilite prin comanda FORMAT, în momentul formatării discului. Numărul de intrări în DIR şi poziţia tabelei în disc sunt memorate în blocul de parametri ai BIOS-ului. Dacă discul este de sistem, primele două intrări din DIR se referă la fişiere care conţin sistemul de operare (aceste fişiere sunt încărcate în memoria principală de către încărcător, care se află în sectorul zero al discului). ♦ Zona de date a fişierelor (FILE) conţine partea de date a acestora şi subdirectoarele. Subdirectoarele sunt tratate ca fişiere, cu articole speciale, de câte 32 octeţi. Structura intrărilor în subdirectoare este identică cu cea a intrărilor în director. Numărul intrărilor în subdirectoare este nelimitat (spre deosebire de intrările în director). Sectoarele din zona de date sunt grupate în clustere care au câte un cuvânt corespondent în FAT. Când un fişier (subdirector) este extins, se caută în FAT clustere libere (cuvinte cu conţinutul 000016) şi se prelungeşte lanţul de alocare. Dacă discul este de sistem, fişierele IO.SYS şi MSDOS.SYS sunt primele în zona de date.

8.2 Structura intrărilor în [sub]directoare Fiecărui fişier sau subdirector îi corespunde o intrare (etichetă) în [sub]directorul părinte. Intrările au lungimea 32 octeţi. Subdirectoarele se comportă ca nişte fişiere cu articole speciale, gestionate de DOS. Structura unei intrări este prezentată în tabelul 8.1. ♦ Numele fişierului este format din 8 caractere, primul având următoarele semnificaţii: • 00 Intrarea în director nu a fost folosită; • 2E Intrarea corespunde unui [sub]director. 2E16 reprezintă caracterul ASCII ".". Zona 1A-1B a intrării conţine mărimea subdirectorului, exprimată în numere de clustere. Dacă şi octetul 01 al intrării este tot 2E16, zona 1A-1B conţine adresa de început în FAT a lanţului de alocare a subdirectorului părinte (zona 1A-1B conţine 000016 dacă părintele este directorul rădăcină). În ambele situaţii, octeţii 01-0A, respectiv 02-0A conţin caracterul spaţiu; • E5 Fişierul a fost şters; • Altă valoare - Primul caracter al numelui de fişier (subdirector). Din analiza structurii numelui se poate deduce că se selectează următoarele tipuri de intrări: intrare normală de fişier (subdirector) fiu; intrare de tip "."; intrare de tip "..". O mai bună precizare a tipului de intrare se realizează prin câmpul de atribute.

145

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

Tabelul 8.1 Structura intrărilor în [sub]director Octeţi

Semnificaţie

00 - 07

Numele fişierului

08 - 0A

Extensia fişierului

0B

Atributele fişierului

0C - 15

Octeţi rezervaţi

16 - 17

Ora ultimei scrieri în fişier

18 - 19

Data ultimei scrieri în fişier

1A - 1B

Adresa de început în FAT a lanţului de alocare a fişierului

1C - 1F

Dimensiunea fişierului, în număr de octeţi

♦ Atributele fişierului au semnificaţia din tabelul 8.2. Prin setarea sau ştergerea corespunzătoare a biţilor octetului 0B pot fi obţinute şi alte combinaţii de atribute (cea mai utilizată este valoarea 0316, care ar corespunde unui fişier readonly ascuns). Însumarea tuturor valorilor (3F16) desemnează un fişier oarecare (orice fişier). ♦ Ora ultimei scrieri în fişier este memorată pe doi octeţi şi are structura din figura 8.2, unde: O reprezintă ora, cu valori binare cuprinse în intervalul [0,23]; M reprezintă minutul, cu valori binare cuprinse în intervalul [0,59]; S reprezintă numărul de incremente de două secunde, exprimat în binar. ♦ Data ultimei scrieri în fişier este memorată pe doi octeţi şi are structura din figura 8.3, unde:

Fig. 8.2 Structura orei ultimei scrieri în fişier

Fig. 8.3 Structura datei ultimei scrieri în fişier

146

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

• A este anul relativ la 1980, cu o valoare binară din intervalul [0-119]; • L este luna exprimată în binar (valori din intervalul [1,12]); • Z este ziua exprimată în binar (valori din intevalul [1,31]). Atât pentru ora, cât şi pentru data ultimei scrieri există proceduri PASCAL care "împachetează", respectiv "despachetează", informaţia. Tabelul 8.2 Atributele unui fişier (octetul 0B al intrării în [sub]director) Valoarea octetului 0B Hexa Biţi

Semnificaţie

76543210 00000000

00

Fişier obişnuit. Asupra lui se pot realiza operaţii de citire şi scriere.

00000001

01

Fişier readonly. Fişierul este protejat la scriere. Din el se poate doar citi. Fişierul nu poate fi şters.

00000010

02

Fişier ascuns (hidden). Fişierul nu poate fi găsit prin operaţii obişnuite de căutare în directori. Nu poate fi afişat prin comanda DIR din DOS şi nu este disponibil prin comenzile COPY, DEL, REN, XCOPY.

00000100

04

Fişier de sistem (system). Se referă la sistemul de operare şi este un fişier ascuns.

00001000

08

Identificator de volum. Un singur fişier, aflat în directorul rădăcină, are acest atribut.

00010000

10

[Sub]director (intrarea se referă nu la un fişier de date, ci la un [sub]director).

00100000

20

Fişierul este de tip arhivă (archive). Bitul este poziţionat pe valoare 1 ori de câte ori are loc o scriere în fişier. Bitul redevine zero în urma unei procesări cu comenzile BACKUP, RESTORE, XCOPY din DOS.

147

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

♦ Adresa de început în FAT indică adresa primului cuvânt din tabela de alocare aferentă fişierului. Această adresă este mai mare ca unu. Drumul logic, de principiu, parcurs până la accesul la datele unui fişier cu identificatorul C:\D1\FIS este prezentat în figura 8.4.

8.3 Tratarea informaţiilor din etichetele fişierelor

ZONA DIR D1

AFAT

FAT

...

...

FF

Subdirectorul D1 Zona de Da te AFAT

Fig. 8.4 Accesul la datele fişierului C:\D1\FIS Eticheta de fişier (intrarea în [sub]director) conţine câmpuri referitoare la nume, extensie, atribute, data şi ora ultimei scrieri în fişier, adresa de început în FAT, dimensiunea fişierului. Etichetele sunt create la deschiderea cu Rewrite a fişierelor. De regulă, valorile câmpurilor nume, extensie, atribute şi adresa de început în FAT sunt înscrise la crearea fişierului. Data şi ora ultimei scrieri se modifică la fiecare nouă scriere în fişier. Dimensiunea fişierului se modifică la scrierea unui nou articol peste sfârşitul iniţial de fişier (pentru fişierele TEXT la scrierea după o deschidere Append, iar pentru fişierele binare la scrierea unui articol cu pointer mai mare ca FileSize(f) iniţial). Unele câmpuri pot fi modificate explicit, prin funcţii şi proceduri speciale. Astfel, SetFAttr poziţionează atributele, SetFTime poziţionează data şi ora, Rename modifică numele [şi extensia], Truncate modifică lungimea. Eticheta de fişier poate fi ştearsă cu procedura Erase (se şterge intrarea în [sub]director).

148

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

8.3.1 Prelucrarea atributelor unui fişier În unit-ul Dos sunt definite proceduri pentru poziţionarea sau citirea atributelor unui fişier. Corespunzător valorilor pe care le poate lua octetul 0B din intrarea unui fişier în [sub]director, în unit-ul Dos sunt definite următoarele constante: ReadOnly = $01 ¾ fişier numai pentru citire; Hidden = $02 ¾ fişier ascuns; SysFile = $04 ¾ fişier de sistem; Volumid = $08 ¾ identificator de volum; Directory = $10 ¾ [sub]director; Archive = $20 ¾ fişier arhivă; AnyFile = $3F ¾ orice fişier Fişierelor de date li se pot asocia atributele $01, $02, $20. În Turbo Pascal, un fişier deschis are, în mod implicit, atributul $20 (arhivă) deoarece în el s-a scris sau urmează să se scrie un articol. Fişierele binare cu atributul $01 pot fi prelucrate numai dacă sunt deschise cu Reset şi dacă variabilei FileMode din unit-ul System i se atribuie, în program, valoarea zero. Fişierele TEXT cu atributul $01 pot fi doar citite (suportă numai deschiderea Reset). Setarea pe o anumită valoare înseamnă, uneori, cumularea de atribute. De exemplu, dacă unui fişier cu atributul $10 (subdirector) i se setează valoarea 2 (hidden), se obţine un subdirector ascuns (atributul $18). ♦ Procedura GetFAttr returnează atributele unui fişier. Ea este definită astfel: GetFAttr(VAR f; VAR attr:WORD) F este variabila care indică fişierul, iar attr este variabila în care procedura depune valoarea atributelor. Exemplu: 8.3. În secvenţa care urmează se citesc atributele fişierului extern 'TEST.DAT' şi se afişează valorile acestora: USES Dos; VAR f:TEXT; i:BYTE; BEGIN ............................. Assign(f,'TEST.DAT'); GetFAttr(f,i); Writeln(i); Reset(f); .............................

♦ Procedura SetFAttr poziţionează (setează) atributele unui fişier. Ea este

149

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

definită astfel: SetFAttr(VAR f; VAR attr:WORD) F este variabila care indică fişierul, iar argumentul real corespunzător lui attr este o expresie a cărei valoare reprezintă atributele şi care va fi înscrisă extern în intrarea ataşată fişierului în [sub]director. Exemple: 8.4. În secvenţa care urmează, fişierului TEST1.DAT i se asociază atributul Readonly, iar fişierului TEST2.DAT atributul Hidden: USES Dos; VAR f1:TEXT; f2:FILE OF READ; BEGIN ........................ Assign(f1,'TEST1.DAT'); SetFAttr(f1,1); Reset(f1); ........................ Assign(f2,'TEST2.DAT'); SetFAttr(f2,Hidden); Reset(f2); .......................

8.5. În programul care urmează, un subdirector capătă atributul ascuns. După setare, valoarea atributelor subdirectorului este 18 (Directory+hidden). USES Dos; VAR f:FILE; nume_dir:STRING[79]; BEGIN Write('Director [n:\cale\director] : '); Readln(nume_dir); Assign(f,nume_dir); SetFAttr(f,2); END.

Procedurile GetFAttr şi SetFAttr au funcţii echivalente cu comanda ATTRIB din DOS.

8.3.2 Prelucrarea datei şi orei din eticheta unui fişier În unit-ul Dos sunt definite proceduri pentru poziţionarea sau citirea datei şi orei ultimei scrieri în fişier. Procedurile prelucreză câmpurile $16-$17 şi $18-$19 din eticheta fişierului (intrarea lui în [sub]director), ca o singură valoare de tip LONGINT. "Împachetarea", respectiv "despachetarea" în/din LONGINT se pot realiza cu procedurile PackTime, respectiv UnpackTime, definite în unit-ul Dos. Aceste proceduri utilizează tipul de date DateTime predefinit în unit-ul Dos, astfel:

150

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

DateTime = RECORD Year : WORD; Month : WORD; Day : WORD; Hour : WORD; Min : WORD; Sec : WORD; END; ♦ Procedura PackTime "împachetează" un articol de tip DateTime într-o dată de tip LONGINT. Ea este definită astfel: PackTime(VAR Dc:DateTime; VAR l:LONGINT) Dc este data şi ora, exprimate pe componente, care urmează să fie "împachetate" sub formă LONGINT în zona Dl. ♦ Procedura UnpackTime "despachetează" o valoare de tip DateTime. Ea este definită astfel: UnpackTime(Dl:LONGINT, VAR Dc:DateTime) Dl este expresie de tip LONGINT care va fi "despachetată" pe componentele datei şi orei în zona Dc. ♦ Procedura GetFTime returnează data şi ora ultimei scrieri într-un fişier. Ea este definită astfel: GetFTime(VAR f; VAR Dl:LONGINT) F este variabila care indică fişierul, iar Dl este variabila în care se recepţionează data şi ora, sub formă "împachetată". Despachetarea lui Dl se face cu procedura UnpackTime. Exemplu: 8.6. USES Dos; VAR f1: FILE OF REAL z: DateTime; data_ora: LONGINT; BEGIN ..................... Assign(f1,'A:TEST.DAT'); GetFTime(f1, Data_ora); UnpackTime(data_ora,z); IF Z.Year < 1994 THEN Writeln('>> Varianta veche de fisier') ELSE Writeln('>> Varianta OK');

♦ Procedura SetFTime înscrie data şi ora în eticheta fişierului. Ea este definită astfel: SetFTime(VAR f; Dl:LONGINT) F este variabila care indică fişierul, iar Dl este variabila care conţine data şi ora

151

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

sub formă împachetată, care se înscriu în intrarea fişierului în [sub]director. Forma împachetată poate fi obţinută cu procedura PackTime. Exemplu: 8.7. USES Dos; VAR f1:FILE OF REAL; z:DateTime; data_ora:LONGINT; BEGIN ........................ ASSIGN (f1,'A:TEST.DAT'); z.Year:=1993; z.Month:=10; z.Day:=28; z.Hour:=11; z.Min:=20; z.Sec:=0; PackTime(z,Data_ora); SetFTime(f1,Data_ora); ..........................

8.3.3 Ştergerea fişierelor În unit-ul System este definită procedura Erase care realizează ştergerea unui fişier existent (şterge eticheta din director). Declaraţia ei este: Erase(VAR f) Dacă fişierul extern, asignat lui f, nu există, execuţia procedurii generează eroare de I/E (IOResult are valoare diferită de zero). Procedura Erase nu se poate executa asupra unui fişier deschis. Exemplu: 8.8. Se citesc 100 de numere x. Elementele pozitive sunt memorate în fişierul cu tip VECTOR.DAT. Dacă toate numerele sunt negative, fişierul VECTOR.DAT nu trebuie să existe. VAR Fis:FILE OF REAL; x:REAL; i,j:1..100; s:BOOLEAN; BEGIN Assign(Fis,'VECTOR.DAT'); Rewrite(Fis); j:=FALSE; FOR i:=1 TO 100 DO BEGIN Readln(x) IF x[i] >= 0 THEN BEGIN Write(Fis,x); j:=TRUE; END; END; Close(Fis); IF NOT j THEN Erase(Fis); END.

152

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

8.3.4 Redenumirea fişierelor Redenumirea unui fişier existent (modificarea numelui fişierului din etichetă) se realizează cu procedura Rename, declarată în unit-ul System, astfel: Rename(VAR f; nume_nou:STRING) Numele intern, f, trebuie asignat anterior unui fişier (vechi) prin procedura Assign. Procedura Rename va redenumi acest fişier extern cu nume_nou (care este o expresie de tip STRING, cu lungimea maximă de 80 caractere). Procedura nu copiază fişiere, ci doar le redenumeşte. De aceea, unitatea şi calea fişierului vechi nu pot fi modificate. Procedura nu se aplică fişierelor deschise. Dacă fişierul extern, asociat lui f prin Assign (fişierul vechi), nu există, execuţia procedurii generează eroare de I/E (IOResult are valoare diferită de zero). Dacă pe unitatea şi în [sub]directorul unde este memorat fişierul vechi există deja un fişier cu numele nume_nou, procedura Rename produce eroare de I/E.

8.3.5 Trunchierea fişierelor În unit-ul System este definită procedura Truncate, care trunchiază un fişier binar de la poziţia curentă a pointerului. Ea este definită astfel: Truncate(VAR f) Procedura şterge din fişier articolele începând din poziţia curentă a pointerului, până la sfârşit, modificând lungimea fişierului din etichetă. După execuţia ei, funcţia Eof(f) va returna valoarea TRUE.

8.4 Crearea şi manipularea [sub]directoarelor În unit-ul System există o serie de proceduri pentru crearea şi ştergerea [sub]directoarelor, pentru definirea [sub]directorului curent şi pentru obţinerea valorii căii curente. Toate aceste proceduri au echivalent în comenzi DOS şi funcţionează identic cu acestea. În plus, având în vedere că subdirectoarele sunt tratate ca fişiere cu articole speciale, asupra lor pot fi aplicate funcţiile referitoare la citirea şi setarea atributelor, datei şi orei creării subdirectorului. ♦ Procedura GetDir returnează calea completă a subdirectorului curent (pathul), dintr-o unitate de disc precizată. Declaraţia ei este: GetDir(Drive:BYTE; VAR s:STRING) Drive specifică unitatea de disc asupra căruia se aplică procedura şi poate avea valorile: 0 - unitatea de disc curentă; 1 - unitatea de disc A; 2 -unitatea de disc B; 3 - unitatea de disc C etc.

153

Programarea calculatoarelor– Tehnica programării în limbajul Pascal

S este variabila în care se recepţionează calea completă a subdirectorului curent, începând cu rădăcina. Forma căii returnate este n:cale, unde n este numele unităţii de disc. Procedura este echivalentă cu comanda CHDIR (fără parametri) din DOS. Exemple: 8.9. Dacă unitatea C: ar avea structura de directori din figura 1.4 şi dacă directorul curent este D4, atunci procedura GetDir(3,Cale) ar returna, în variabila Cale, şirul: C:\D2\D4; 8.10. Procedura GetDir (9,Cale) returnează, în variabila Cale, şirul I:\, chiar dacă unitatea I:\ nu a fost definită pe discul fix. ♦ Procedura ChDir schimbă directorul curent. Ea are următoarea declaraţie: ChDir(s:STRING) S conţine un şir de forma [n:][cale] care defineşte noul subdirector curent. Calea poate fi precizată complet, pornindu-se de la rădăcină sau într-un mod relativ, folosindu-se formele construite cu următoarele caractere: \ (directorul rădăcină); . (directorul curent); .. (directorul părinte al celui curent). Exemplu : 8.11. Dacă în structura de directoare din figura 1.4, directorul curent este D4 şi se doreşte ca D3 să devină curent, se poate proceda astfel: ChDir('C:\D3'); sau ChDir('\D3'); sau ChDir('..'); ChDir('..'); ChDir('D3'); Când calea este specificată greşit se generează eroare (path not found). Procedura are funcţie echivalentă cu comanda CHDIR din DOS. ♦ Procedura MkDir creează un director în disc. Ea are următoarea declaraţie: MkDir(s:STRING) S conţine un şir de forma [n:]cale care defineşte subdirectorul care se creează. Lungimea maximă a căii, de la rădăcină până la nivelul dorit, nu trebuie să depăşească 63 caractere, inclusiv caracterele \. Calea poate fi precizată complet, pornindu-se de la rădăcină sau relativ, folosindu-se formele prescurtate construite cu caracterele \, ., ..(vezi ChDir). Procedura are funcţie echivalentă cu comanda MKDIR din DOS. Exemplu: 8.12. Dacă în structura de director din figura 1.4 se doreşte crearea unui subdirector D6, subordonat lui D3, se poate proceda astfel: MkDir('C:\D3\D6') ¾ de oriunde; MkDir('\D3\D6') ¾ de oriunde din unitatea C; MkDir('D6') ¾ din D3;

154

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

♦ Procedura RmDir şterge un subdirector gol. Ea are următoarea definiţie: RmDir(s:STRING) S conţine un şir de forma [n:]cale ce desemnează subdirectorul care se şterge. Calea poate fi specificată complet, pornindu-se de la rădăcină sau relativ, folosindu-se formele prescurtate construite cu caracterele \, ., .. (vezi ChDir). Subdirectorul care se şterge nu trebuie să conţină fişiere sau subdirectoare ataşate. În cazul în care subdirectorul nu există sau nu este gol se produce eroare de I/E. Directorul rădăcină şi subdirectorul curent nu pot fi şterse. Procedura are funcţie echivalentă cu comanda RMDIR din DOS. Exemplu: 8.13. Se propune o procedură care asigură realizarea următoarelor operaţii, în funcţie de valoarea unui parametru (op): determinarea directorului curent, schimbarea directorului curent, crearea unui director. Procedura se construieşte în cadrul unui unit (GestDir), care poate fi extins şi cu alte proceduri de interes general în lucrul cu directoare şi/sau fişiere. UNIT GestDir; INTERFACE PROCEDURE OpDir(op:CHAR; VAR d:STRING; VAR rez:INTEGER); IMPLEMENTATION PROCEDURE OpDir; BEGIN CASE UpCase(op) OF 'D': GetDir(0,d); 'S': BEGIN {$I-} ChDir(d); {$I+} rez:=IOResult; END; 'C': BEGIN {$I-} MkDir(d); {$I+} rez:=IOResult; END; END; END; END.

În continuare se prezintă un apelator al subprogramului, prin care se urmăreşte schimbarea directorului curent, cu salvarea directorului în uz de la momentul lansării, respectiv restaurarea lui la încheierea execuţiei programului. Dacă noul director nu există, se asigură posibilitatea creării lui, urmată de transformarea în director curent, în aceleaşi condiţii. Specificatorul noului director este furnizat ca parametru în linia de comandă cu care este lansat în execuţie programul. Numărul parametrilor de pe linia de comandă este determinat prin funcţia ParamCount:WORD. Accesul la un anumit parametru se face prin funcţia ParamStr(n:WORD):STRING, unde n este numărul de ordine al parametrului în lista din linia de comandă. PROGRAM ExDir; USES GestDir;

155

Programarea calculatoarelor– Tehnica programării în limbajul Pascal VAR r : INTEGER; c : CHAR; dinc,dnou: STRING; PROCEDURE DetDir; VAR dir: STRING; BEGIN OpDir('d',dir,r); WriteLn('Director curent: ',dir); END; BEGIN WriteLn('Program in executie curenta: ',ParamStr(0)); DetDir; IF ParamCount <> 0 THEN BEGIN dnou:=ParamStr(1); OpDir('d',dinc,r); OpDir('s',dnou,r); IF r <> 0 THEN BEGIN WriteLn('Director inexistent: ',ParamStr(1)); Write('Se creeaza ca director nou?(Y/N): '); ReadLn(c); IF UpCase(c) = 'Y' THEN BEGIN OpDir('c',dnou,r); IF r = 0 THEN OpDir('s',dnou,r) ELSE BEGIN WriteLn('>> Eroare la creare director. Executie intrerupta!' ); RunError(r); END; END; END; END; DetDir; END; {restul programului} IF ParamCount <> 0 THEN OpDir('s',dinc,r); DetDir; END.

8.5 Căutarea fişierelor în [sub]directoare În unit-ul Dos sunt definite proceduri care caută un fişier într-un [sub]director sau într-o listă de [sub]directore. Procedurile utilizează tipurile de data PathStr şi SearchRec, definite în unit-ul Dos, astfel: PathStr=STRING[79]; SearchRec = RECORD Fill : ARRAY[1..21] OF BYTE; Attr : BYTE; Time: LONGINT; Size : LONGINT;

156

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

Name : STRING[12]; END; Semnificaţia câmpurilor tipului SearchRec este următoarea: Fill: rezervat pentru sistemul de operare. Câmpul nu trebuie modificat în program. Attr: atributele fişierului (valoarea octetului $0B din intrarea fişierului în [sub]director). Time: data şi ora ultimei scrieri în fişier, sub formă împachetată (valoarea, ca LONGINT, a octeţilor $16-$19 din intrarea fişierului în [sub]director). Size: lungimea, în octeţi, a fişierului (valoarea câmpului $1C-$20 din intrarea fişierului în [sub]director). Name: numele fişierului urmat de punct şi extensia lui (valoarea câmpurilor $0-$7 şi $8-$A din intrarea fişierului în [sub]director). ♦ Procedura FindFirst caută într-un [sub]director prima intrare a unui fişier care are specificatorul şi atributul precizate în lista de parametri. Ea are declararea: FindFirst(F_extern:PathStr; Attr:Word; VAR zona:SearchRec) F_extern este specificatorul extern al fişierului care este căutat. Specificatorul este format din cale, nume fişier şi, eventual, extensie. Când calea lipseşte se presupune [sub]directorul curent. Numele şi extensia fişierului pot fi globale (formate cu caracterele * sau ?). Attr reprezintă valoarea atributelor fişierului care se caută. Zona este o variabilă de tipul SearchRec, definit în unit-ul Dos, care va conţine informaţii despre fişierul f_extern, în cazul în care a fost găsit. Dacă f_extern nu este găsit, variabila zona rămâne nemodificată. Când f_extern este găsit, variabila DosError (definită în unit-ul Dos) va avea valoarea zero. În caz contrar, DosError are valoare diferită de zero (vezi §8.3). ♦ Procedura FindNext caută într-un [sub]director următoarea intrare a unui fişier care are specificatorul şi atributul precizate la apelul anterior al procedurii FindFirst. De aici rezultă faptul că procedura FindNext poate fi utilizată numai dacă, anterior, a fost apelată procedura FindFirst. Procedura este definită astfel: FindNext(VAR zona:SearchRec) Zona are aceeaşi semnificaţie ca la procedura FindFirst. Dacă următoarea intrare este găsită, variabila DosError (definită în unit-ul Dos) va avea valoarea zero. În caz contrar, DosError are valoare diferită de zero. Exemplu: 8.14. Următorul program afişează toate fişierele cu extensia .DAT dintr-o cale precizată de la tastatură. Pentru fiecare fişier se afişează: numele, extensia, atributul, lungimea, data şi ora ultimei scrieri în fişier. PROGRAM EX14; USES Dos; VAR Cale:String[79]; Zona:SearchRec;

{Tip definit in Dos}

157

Programarea calculatoarelor– Tehnica programării în limbajul Pascal Dc:DateTime; {Tip definit in Dos} BEGIN Write('Calea de cautat ([u:]\d1\d2\):'); Readln(Cale); {Se citeste calea de la tastatura; pentru directorul curent se poate tasta ENTER} FindFirst(Cale+'*.DAT',$3F,Zona); {Cautarea primului fisier cu extensia .DAT} IF DosError <> 0 THEN Writeln('>> Nu exista fisier de tip .DAT') ELSE REPEAT UnpackTime(Zona.Time,Dc); Writeln(Zona.Name:12,'',Zona.Attr:2,'',Zona.Size:10,'',Dc.Year :4,':',Dc.Month:2,':',Dc.Day:2,'',Dc.Hour:2,'H',Dc.Min:2,'M',Dc. Sec:2,'S'); FindNext(Zona); Until DosError=18; END.

♦ Funcţia FSearch caută un fişier într-o listă de [sub]directori. Ea este asemănătoare comenzii PATH din DOS. Funcţia este definită astfel: FUNCTION FSearch(Nume:PathStr; Lista:STRING):PathStr Nume este variabilă de tip PathStr (definită în unit-ul Dos), care conţine numele fişierului de căutat. Lista conţine lista directoarelor în care se continuă căutarea, dacă fişierul nu a fost găsit în directorul curent. Căile specificate în listă trebuie separate prin caracterul ;. Funcţia returnează specificatorul extern al fişierului, în cazul în care îl găseşte. Când fişierul este în directorul curent, specificatorul extern furnizat este format din nume şi, eventual, extensie, iar, când fişierul este în alt director, specificatorul este complet (cale + nume [+ extensie]). Când fişierul nu este găsit, funcţia returnează şirul vid. Funcţia nu verifică existenţa căilor. Când căile nu există, se returnează tot şirul vid. Exemplu : 8.15. Programul care urmează caută un fişier într-o listă de directori. Când fişierul este găsit se afişează specificatorul extern. În caz contrar, se afişează mesajul >>Fişier negăsit. PROGRAM EX15; USES Dos; VAR Nume:PathStr; x:STRING; BEGIN Write('Numele extern al fisierului cautat (xxxxxxxx.eee):'); Readln(Nume); x:=FSearch(Nume,'C:\wp51\;\wp51\rosca\;\tp\'); IF x[0] = #0 THEN Writeln('>>Fisier negasit') ELSE Writeln('>>Fisierul are specificatorul extern : ',x) END.

158

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

8.6 Tratarea şirului referitor la specificatorul extern de fişier În unit-ul Dos sunt definite o funcţie şi o procedură care prelucrează şirul de caractere referitor la specificatorul extern al unui fişier. Funcţia (FExpand) şi procedura (FSplit) nu fac nici o verificare sau căutare în suportul extern. Ele fac parte, mai degrabă, din categoria prelucrării unor şiruri de caractere, decât din cea de prelucrare a fişierelor. În definirea funcţiei şi procedurii se folosesc următoarele tipuri de date declarate în unit-ul Dos: PathStr = STRING[79]; ¾ Specificator complet; DirStr = STRING[67]; ¾ Unitate şi cale; NameStr = STRING[8]; ¾ Numele propriu-zis; ExtStr = STRING[4]; ¾ Extensie. ♦ Funcţia FExpand expandează (completează) numele cu calea. Ea este definită astfel: FUNCTION FExpand(Nume:PathStr):PathStr Nume este variabilă de tip PathStr care conţine numele ce urmează să fie expandat cu calea. În procesul prelucrării fişierelor, funcţia are sens când este utilizată pentru extinderea cu componente (unitate, cale) furnizate implicit. Exemple: Se consideră directorul curent C:\TP 8.16. FExpand('Test1.DAT') ¾ returnează C:\TP\TEST1.DAT 8.17. FExpand('\TP\Test1.DAT')

¾ returnează C:\TP\TEST1.DAT

8.18. FExpand('\Wp51\Test1.DAT') ¾ returnează C:\WP51\TEST1.DAT 8.19. FExpand('A:TEST1.DAT')

¾ returnează A:TEST1.DAT

8.20. Programul care urmează caută un fişier într-o listă de directori. Când fişierul este găsit se afişează specificatorul extern. În caz contrar se afişează mesajul >>Fişier negăsit. Deosebirea faţă de exerciţiul 15 constă în aceea că, în cazul în care fişierul este găsit în directorul curent, se scrie specificatorul complet (n:\cale\nume_fişier.extensie). PROGRAM EX20; USES Dos; VAR Nume:PathStr; x:STRING; BEGIN Write('Numele extern al fisierului cautat (xxxxxxxx.eee):'); Readln(Nume); x:=FSearch(Nume,'C:\wp51\;\wp51\rosca\;\tp\');

159

Programarea calculatoarelor– Tehnica programării în limbajul Pascal IF x[0] = #0 THEN Writeln('>>Fisier negasit') ELSE Writeln('>>Fisierul are specificatorul ',FExpand(x)) END.

extern

:

♦ Procedura FSplit descompune specificatorul extern (format din unitate, cale, nume, extensie) în trei componente: unitate+cale, nume, .extensie. Procedura este definită astfel: FSplit(specificator:PathStr;VAR unit_dir:DirStr;VAR name:NumeStr;VAR ext:ExtStr) Specificator este şirul care se analizează şi se descompune în componente. Unit_dir, nume şi ext sunt variabile în care se depun cele trei componente extrase din specificator. Când unitatea şi calea nu sunt prezente în specificator, se returnează valorile implicite. Când numele şi extensia nu sunt prezente în specificator, se returnează şiruri vide. Exemplu: 8.21. Fie un program care lucrează cu un fişier al cărui nume este introdus de la tastatură. În cazul în care de la tastatură se introduce <ENTER>, se lucrează cu fişierul implicit EX.DAT. PROGRAM EX21; USES Dos; VAR f:FILE OF REAL; spec:PathStr; u:DirStr; n:NameStr; e:ExtStr; BEGIN Write('Nume fi•ier (EX.DAT):'); Readln(Spec); FSplit(Spec, u, n, e); IF n = '' THEN n:='Ex'; IF e = '' THEN e:='.DAT'; Spec:=u+n+e; Assign(f, spec); Reset(f); {Prelucrare} Close (f); END.

8.7 Funcţii pentru determinarea capacităţii şi a ocupării discului În unit-ul Dos există definite două funcţii care returnează capacitatea unui disc şi spaţiul neocupat din el. Dacă funcţiile se aplică unităţilor de disc flexibil, în cazul în care nu este montat discul, se generează un mesaj de sistem care solicită acest lucru (afirmaţia este valabilă pentru toate funcţiile şi procedurile Pascal care referă unităţi de discuri flexibile).

160

Prelucrarea etichetelor de fişier şi a altor informaţii externe memorate în discuri

♦ Funcţia DiskSize returnează capacitatea totală, în octeţi, a discului montat în unitatea specificată. Ea are declaraţia: Function DiskSize(Drive:BYTE):LONGINT Drive specifică unitatea de disc, astfel: 0 - unitatea de disc curentă; 1 - unitatea de disc A; 2 - unitatea de disc B; 3 - unitatea de disc C etc. Funcţia returnează valoarea -1 dacă unitatea de disc nu există, dacă nu este montat disc în unitate sau dacă unitatea este defectă. ♦ Funcţia DiskFree returnează numărul de octeţi liberi de pe un disc montat într-o unitate. Ea are declaraţia: Function DiskFree(Drive:BYTE):LONGINT Drive are aceeaşi semnificaţie ca la funcţia DiskSize. Funcţia returnează valoarea -1 dacă unitatea de disc nu există, dacă nu este montat disc în unitate sau dacă unitatea este defectă. Exemplu: 8.22. Se presupune că urmează să fie creat un fişier, MAT.DAT, care are aproximativ 10000 octeţi. Se poate testa, cu o oarecare aproximare, dacă fişierul încape pe un anumit disc (fie el C:). Trebuie reţinut faptul că octeţii liberi sunt grupaţi în clustere libere. USES Crt; ......... BEGIN ......... IF DiskFree(3) = -1 THEN Writeln('>>Unitatea C defecta') ELSE IF DiskFree(3) > 10000 THEN {Asignare, deschidere, creare} ELSE Writeln('>>Spatiu insuficient pentru fisier'); ..........

161

UNELE ASPECTE TEHNICE REFERITOARE LA PRELUCRAREA FIŞIERELOR

Înţelegerea corectă a mecanismelor referitoare la prelucrarea fişierelor necesită cunoaşterea unor detalii tehnice de realizare a operaţiilor de I/E. Câteva din ele sunt prezentate în continuare, altele în anexa 3.

9.1 Realizarea fizică a transferului de date Procedurile şi funcţiile de I/E asigură, prin intermediul DOS, trecerea dintre nivelul logic, specific utilizatorului, care consideră fişierul ca o succesiune de articole (blocuri, linii, câmpuri) şi nivelul fizic de organizare a datelor, în raport cu care fişierul apare ca o succesiune de octeţi. În suportul extern magnetic (disc), înregistrarea datelor se face pe număr întreg de sectoare. Sub sistemul de operare MS-DOS, sectorul are 512 octeţi şi reprezintă unitatea de transfer cu memoria principală. Fiecărui fişier îi sunt alocate două tipuri de zone de memorie internă: zonă utilizator şi zonă tampon. Zona utilizator este descrisă în VAR sau CONST. La ea are acces programul utilizatorului. Zonă are semnificaţii diferite, în funcţie de tipul fişierului care se prelucrează. Ea este fie un articol (la fişiere cu tip), fie un bloc (la fişiere fără tip), fie este o mulţime de zone independente care sunt specificate în operaţiile de citire/scriere (la fişierele TEXT). Zona tampon (buffer) reprezintă zonă de memorie principală în/din care se face transferul datelor din/în exterior şi are, sub MS-DOS, 528 octeţi. Un sistem poate lucra la un moment dat cu mai multe buffer-e, numărul lor fiind stabilit la configurarea sistemului (comanda BUFFERS din MS-DOS). Cu cât numărul de buffere este mai mare cu atât creşte viteza de transfer, dar şi memoria internă ocupată. La citiri succesive, sectoarele sunt încărcate în buffere "eliberate" şi de aici în zonele utilizator. Dacă, de exemplu, se citeşte un articol de 120 octeţi, sistemul citeşte în buffer un sector întreg şi mută, de aici, în zonă utilizator, 120 octeţi. La următoarea citire, sistemul va utiliza alt

162

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

buffer etc., astfel încât, la un moment dat, buffer-ele vor conţine cele mai recente date citite. Dacă citirea se face aleator (nu secvenţial), se încarcă în buffer(e) sectorul/sectoarele (ca întregi) care conţin(e) articolul. Dacă articolul este deja în buffer(e), nu are loc transfer din exterior ci numai din buffer(e) în zonă utilizator. Succesiunea de principiu a operaţiilor, în cazul unui flux general de date care implică un singur buffer, este prezentată în figura 9.1. Ea este următoarea: 1. citirea unui sector din fişierul de intrare în zonă tampon asociată; 2. transferul datelor din buffer în zonă utilizator asociată fişierului de intrare; 3. pregătirea conţinutului zonei utilizatorului asociată fişierului de ieşire, pe baza datelor preluate din zona fişierului de intrare sau din alte surse.

Fig.9.1 - Fluxul general de date în operaţiile de I/E În limbajul PASCAL, aceeaşi zonă utilizator poate fi folosită atât pentru fişierul de intrare, cât şi pentru cel de ieşire; 4. Transferul datelor din zona utilizator în buffer-ul fişierului de ieşire; 5. Scrierea în fişierul de ieşire a sectorului (când este completat), din zona tampon. Cu toate că procesul de trecere dintre nivelurile fizic şi logic are trăsături principale comune, există deosebiri esenţiale de realizare pentru fişierele TEXT şi cele binare. Pentru fişierele binare (cu tip şi fără tip) se poate considera valabilă schema de principiu din figura 9.1. Transferul intern dintre buffer şi zonă utilizator (operaţiile 2 şi 4) are loc fără conversii, iar operaţiile de intrare/ieşire dintr-un program pot avea loc pe acelaşi fişier. Pentru fişierele TEXT, o prima particularitate constă în aceea că datele sunt transferate în/din una sau mai multe zone de memorie independente şi neomogene ca tip (figura 9.2).

163

Unele aspecte tehnice referitoare la prelucrarea fişierelor

Fig.9.2 - Principiul transferului datelor în cazul fişierelor TEXT

În plus, datele sunt "decupate" din zona buffer în zonele de date, pe baza unor caractere cu rol de separator sau după alte reguli. Pentru datele numerice (întregi şi reale), transferul din/în zonele buffer în/din zonele de date (operaţiile 2 şi 4 din figura 9.2) are loc cu conversie. Acelaşi lucru se întâmplă la scriere şi cu datele de tip BOOLEAN. Atât pentru fişierele binare cât şi pentru cele TEXT, operaţiile de transfer din exterior în buffer (1) şi din acesta în zona utilizator (2) au loc, de regulă, ca urmare a execuţiei procedurilor de citire. În unele situaţii, operaţia (1) are loc ca urmare a execuţiei funcţiilor de testare a sfârşitului de fişier. Operaţiile de transfer din zona utilizator în buffer (4) şi din acesta în exterior (5) au loc ca urmare a execuţiei procedurilor de scriere. Operaţiile 1 şi 2 (respectiv 4 şi 5) nu au loc, întotdeauna, simultan. Operaţia 1 are loc numai când buffer-ul de intrare este "eliberat", iar operaţia 5 are loc numai când buffer-ul de ieşire este plin. Procesul se desfăşoară diferit la fişierele binare şi la cele TEXT. ♦ La fişiere binare buffer-ul de intrare este "eliberat" ca urmare a execuţiei unei operaţii de deschidere sau a citirii unui articol/bloc (Read, BlockRead) din fişierul respectiv. Exemplu: 9. 1.

RESET(fis); ¾ buffer "eliberat" .................. Read(fis,art); ¾ realizeaz• opera•iile 1 •i figura 9.1 (buffer "eliberat")

2

din

164

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Funcţia Eof găseşte întotdeauna buffer-ul "eliberat" (fie că este înaintea unui Read, fie că este după acesta), ceea ce determină ca funcţia să efectueze transferul din exterior în zonă buffer (operaţia 1 din figura 9.1). Când procedura Read se execută după apelul funcţiei Eof, atunci ea realizează numai operaţia (2). Exemplu: 9.2. WHILE

NOT

Eof(f)

DO¾

Transfera din (opera•ia 1)

BEGIN Read(F,art);¾

Transfera din (opera•ia 2) (* Prelucrare articol *) END;

fi•ier

buffer

în

în

zon•

zon•

buffer articol

După testarea sfârşitului de fişier, funcţia Eof nu modifică pointerul, acesta rămânând pe începutul buffer-ului. Tentativa de citire a unui articol, când Eof(f)=TRUE, produce eroare de I/E (IOResult diferit de 0). Buffer-ul de ieşire se consideră plin la fiecare scriere de articol/bloc. Procedurile Write şi BlockWrite realizează întotdeauna operaţiile 4 şi 5 din figura 9.1. ♦ La fişierele TEXT buffer-ul de intrare se consideră "eliberat" după execuţia unei operaţii de deschidere (implicită sau explicită) sau când pointerul avansează în buffer după sfârşitul de linie (CR/LF) sau după marcatorul de sfârşit de fişier (CTRL/Z). Exemple: 9.3. Se presupun trei variabile a, b, c, de tip numeric şi secvenţa de program: Read(a); {primul Read din program} Read(b); Read(c);

Înaintea primului Read, buferul de intrare este "eliberat" (datorită deschiderii implicite). De aceea, Read(a) transferă date din exterior în buffer. Se presupune că au fost introduse urmatoarele valori:

165

Unele aspecte tehnice referitoare la prelucrarea fişierelor

Tot procedura Read(a) realizează transferul (cu conversie) din buffer în zona a (a=12). Pointerul se plasează în poziţia 1. Procedura Read(b) găseşte bufferul "neeliberat", deci nu execută transfer din exterior, ci numai din buffer în zona b (b=300). Pointerul se plasează în poziţia 2. Procedura Read(c) găseşte buffer-ul "neeliberat", deci nu execută transfer din exterior, ci analizează caracterele din buffer. Cum la citirea datelor numerice spaţiile şi caracterele CR/LF sunt ignorate, pointerul avansează până în poziţia 3, când buffer-ul devine "eliberat", producându-se în acest moment o cerere de transfer din exterior în buffer etc. În concluzie, secvenţa anterioară realizează: Read(a) Read(b) Read(c)

¾ opera•iile 1 •i 2; ¾ opera•ia 2; ¾ analiza, opera•ia 1 etc..

9. 4. Se presupun trei variabile a, b, c de tip numeric şi secvenţa de program: ReadLn(a) ; {prima citire din program} ReadLn(b) ; ReadLn(c) ;

Înaintea primului ReadLn buffer-ul de intrare este "eliberat" (datorită deschiderii implicite). De aceea, ReadLn(a) transferă date din exterior în buffer. Se presupune că au fost introduse urmatoarele valori:

Tot procedura ReadLn(a) transferă (cu conversie) din buffer în zona a (a=12) şi plasează pointerul la sfârşitul liniei (în poziţia 1), după CR/LF. Procedura ReadLn(b) găseşte buffer-ul "eliberat" şi provoacă cerere de transfer din exterior etc. Funcţia Eof poate găsi buffer-ul "eliberat" sau "neeliberat". Când îl găseşte "eliberat" produce un transfer din exterior în buffer, plasează pointerul pe începutul său şi testează dacă este sfârşit de fişier (CTRL/Z). Dacă funcţia Eof găseşte buffer-ul "neeliberat", testează dacă pointerul indică sfârşitul de fişier. După testarea sfârşitului de fişier, funcţia Eof nu modifică pointerul . Eliberarea buffer-ului unui fişier TEXT, cu scrierea conţinutului său în suportul extern, se poate realiza cu procedura Flush(f). F trebuie deschis pentru scriere. În concluzie, atât pentru fişierele binare, cât şi pentru cele TEXT, trebuie reţinute următoarele observaţii importante: • nu întotdeauna operaţiile Read, ReadLn,

166

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

BlockRead produc transfer din exterior în memoria principală. Transferul are loc numai dacă buffer-ul este "eliberat"; • funcţia Eof realizează şi operaţia de transfer din exterior în memoria principală. Acest lucru se întâmplă când, la execuţia funcţiei, buffer-ul de intrare este "eliberat".

9.2 Accesul la blocul de informaţii despre fişier Declararea variabilei asociate fişierului, de orice tip, are ca efect rezervarea şi iniţializarea parţială de către compilator a unui bloc de informaţii (File Informaţion Block). Unele câmpuri ale blocului primesc valori la execuţia procedurii Assign, iar altele la deschiderea fişierului. Pentru fiecare fişier din program, compilatorul generează câte un FIB. Numele simbolic al blocului coincide cu numele intern asociat fişierului prin descrierea din secţiunea VAR. Informaţiile din FIB sunt utilizate, la execuţie, de procedurile şi funcţiile care realizează operaţii de I/E. Structura FIB este definită în unit-ul Dos prin două declaraţii de tip: FileRec, pentru fişierele binare şi TextRec, pentru fişierele TEXT. ♦ Tipul FileRec reprezintă structura FIB pentru fişiere cu tip şi fără tip. El este definit astfel: FileRec = RECORD Handle : WORD; Mode : WORD; RecSize : WORD; Private : ARRAY[1...16] OF BYTE; UserData : ARRAY[1...16] OF BYTE; Name : ARRAY[0...79] OF CHAR; END; ♦ Tipul TextRec reprezintă structura FIB pentru fişierele TEXT. El este definit astfel: TextRec = RECORD Handle : WORD; Mode : WORD; BufSize : WORD; Private : WORD; BufPos : WORD; BufEnd : WORD; BufPtr : ^TextBuf;

167

Unele aspecte tehnice referitoare la prelucrarea fişierelor

OpenFunc : Pointer; InOutFunc : Pointer; FlushFunc : Pointer; UserData : ARRAY[1...16] OF BYTE; Name : ARRAY[0...79] OF CHAR; Buffer : TextBuf; Semnificaţia principalelor câmpuri este următoarea: • Handle este o valoare întreagă care reprezintă identificatorul pentru fişiere deschise. Valorile 1-7 sunt utilizate pentru dispozitivele standard de I/E (intrare, ieşire, auxiliar, imprimantă, fişierele în curs de folosire cu comanda PRINT din DOS şi de reţea). Fişierele deschise ale utilizatorului au valori pentru handle începând cu 8. Fişierele nedeschise au handle=0. O valoare handle asociată unui fişier devine disponibilă la închiderea acestuia. Exemple: 9.5. Dacă într-un program se lucrează simultan cu trei fişiere, valorile handle asociate sunt 8, 9, 10. 9.6. Dacă într-un program se lucrează cu trei fişiere deschise şi închise pe rând, fiecare va avea valoarea handle opt. Într-un program pot fi deschise simultan maxim 12 fişiere ale utilizatorului (handle cu valori din intervalul [8, 19]). • Mode indică starea fişierului, care poate fi exprimată şi prin următoarele constante definite în unit-ul Dos: FmClosed = $D7B0 ¾ fişier închis; FmInput = $D7B1 ¾ fişier deschis pentru citire; FmOutput = $D7B2 ¾ fişier deschis pentru scriere; FmInOut = $D7B3 ¾ fişier deschis pentru citire/scriere; Fişierele TEXT pot avea stările FmClosed, FmInput, FmOutput. Fişierele binare pot avea orice stare (implicit FmClosed sau FmInOut). În unit-ul System este definită variabila FileMode astfel: FileMode:BYTE=2 Variabila determină modul de deschidere a fişierelor binare de către procedura Reset. Valorile ei posibile sunt: 0 - fişier FmInput; 1 - fişier FmOutput; 2 - fişier FmInOut. Variabila are valoarea implicită 2. Atribuirea altei valori are ca efect folosirea acestui mod de către toate apelurile ulterioare ale procedurii Reset. • RecSize indică lungimea articolului (blocului) rezultată din descrierea internă programului.

168

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

• Name este identificatorul extern al fişierului, aşa cum este precizat de procedura Assign. • BufSize reprezintă lungimea buffer-ului fişierului TEXT. • BufPos, BufEnd, BufPtr reprezintă pointeri folosiţi în gestiunea buffer-elor asociate fişierului TEXT. • OpenFunc, InOutFunc, CloseFunc, FlushFunc reprezintă adresa driver-elor pentru deschidere, citire/scriere, închidere, golire a buffer-ului. • Buffer reprezintă zonă tampon asociată fişierului (chiar buffer-ul fişierului). Tipul TextBuf este definit în unit-ul Dos astfel: TextBuf = ARRAY[0...127] of CHAR. Pentru a avea acces la informaţiile din FIB, trebuie declarată o variabilă de tip FileRec (respectiv TextRec) care să aibă aceeaşi adresă cu FIB-ul (cu variabila de tip fişier): a) Pentru fişiere TEXT: VAR f: TEXT; inf_f: TextRec ABSOLUTE f; b) Pentru fişiere cu tip: VAR f: FILE OF tip; inf_f: FileRec ABSOLUTE f; c) Pentru fişiere fără tip: VAR f: FILE; inf_f:FileRec ABSOLUTE f; Câmpurile zonei inf_f se adresează cu denumirile lor din definirea articolului FileRec, respectiv TextRec, din unit-ul Dos.

9.3 Variabile şi funcţii pentru prelucrarea erorilor de I/E În fiecare dintre unit-urile standard sunt definite variabile şi funcţii care pot semnala modul de desfăşurare a operaţiilor de intrare/ieşire. ♦ În unit-ul System sunt definite următoarele variabile şi funcţii: • Variabila InOutRes conţine codurile de eroare la execuţie, generate de rutinele de I/E, corespunzând unor situaţii cum sunt (anexa 3): eroare la citire de pe disc (codul 100), la scriere pe disc (101), fişier neasignat (102), fişier nedeschis (103), fişier nedeschis pentru intrare (104), fişier nedeschis pentru ieşire (105), format numeric invalid (106). Variabila InOutRes are valoarea zero dacă operaţia de I/E s-a desfăşurat normal. Ea este definită astfel: InOutRes:Integer=0; • Funcţia IOResult returnează programului valoarea variabilei InOutRes şi o

169

Unele aspecte tehnice referitoare la prelucrarea fişierelor

pune pe aceasta pe zero. Dacă valoarea InOutRes este diferită de zero şi directiva de compilare $I are valoarea {$I-}, programul nu se întrerupe, dar următoarea operaţie de I/E nu se va mai executa; dacă are valoarea {$I+} programul se opreşte cu eroare de execuţie. De aceea, dacă se doreşte controlul desfăşurării unei operaţii de I/E, se procedează astfel (pe exemplul procedurii READ): {$I-} {inhibă întreruperea programului} Read (f,zonă); {$I+} {autorizează execuţia urmatoarei operaţii de I/E} IF IOResult <>0 THEN {eroare de I/E} ELSE {nu există eroare de I/E}; • Variabila ErrorAddr conţine adresa unde s-a produs eroarea de execuţie. În cazul inexistenţei erorii, conţine valoarea nil. Variabila este definită astfel: ErrorAddr:pointer=nil. Adresa este memorată sub forma ssss:dddd, unde ssss este adresa segmentului de cod, iar dddd este offset-ul (deplasarea în cadrul segmentului). ♦ În unit-ul Dos sunt definite următoarele proceduri şi variabile: • Variabila DosError, de tip INTEGER, este iniţializată de funcţiile şi procedurile din acest unit, cu următoarele valori principale (vezi anexa 1): 0 - fără eroare; 2 - fişier negăsit; 3 - cale negăsită; 4 - prea multe fişiere deschise; 5 - acces interzis; etc. Variabila DosError are valoarea zero când execuţia functiilor şi a procedurilor definite în unit-ul Dos se termină normal. • Procedura SetVerify poziţionează comutatorul verify din DOS. Comutatorul are două valori posibile: ON (valoare logică TRUE) sau OFF (valoare logică FALSE). Când comutatorul are valoarea ON, sistemul de operare face verificare după fiecare operaţie de scriere în fişiere (se verifică dacă datele scrise pot fi citite fără eroare). Poziţia On a comutatorului verify măreşte timpul de execuţie a programelor. Când comutatorul are valoarea OFF, sistemul de operare nu face verificarea scrierii. Valoarea implicită a comutatorului este OFF. Valoarea comutatorului rămâne activă până la o nouă setare. Procedura este defintă astfel: SetVerify(v:Boolean) V este o variabilă de tip BOOLEAN. Când v are valoarea TRUE, comutatorul verify primeşte valoarea ON. În caz contrar primeşte valoarea OFF. Procedura are efect similar cu comanda VERIFY din DOS. Dacă comutatorul verify este ON, rezultatul verificării scrierii este memorat în variabila DosError. În anexa 3 sunt prezentate principalele erori de execuţie, care includ şi pe cele de intrare/ieşire.

170

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

9.4 Anomalii în tratarea lungimii articolelor (blocurilor) fişierelor binare Deoarece peste un fişier fizic poate fi suprapus orice tip de fişier, rămâne în sarcina programatorului să asigure compatibilitatea cu cerinţele de prelucrare. În cazul fişierelor binare, lungimea şi structura articolelor (blocurilor) pot diferi între momentul creării şi cel al exploatării. Exemplu: 9.7. La momentul creării, o matrice este scrisă câte o linie pe articol: TYPE a=ARRAY[1..10] of REAL; VAR Fis:FILE OF a; Linie:a; ..................... BEGIN Assign(Fis,'MATRICE.DAT'); Rewrite(Fis); ..................... Write(Fis,Linie); .....................

La momentul exploatării, matricea poate fi citită câte un element pe articol: VAR Fis:FILE OF REAL; Element:REAL; ..................... BEGIN Assign(Fis,'MATRICE.DAT'); Reset(Fis); ..................... Read(Fis,Element); .....................

Fie lart şi lbloc lungimea articolelor, respectiv blocurilor unui fişier binar care are lungimea lfis, exprimată în octeţi. O prelucrare corectă a fişierului presupune să fie îndeplinite următoarele condiţii: lfis MOD lart=0, respectiv lfis MOD lbloc=0. În caz contrar se produc anomalii în prelucrare, ultimii lfis MOD lart, respectiv lfis MOD lbloc octeţi, neputând fi prelucraţi. Exemplu: 9.8. În programul demonstrativ care urmează se creează fişierul TEST.DAT care are lungimea 8 octeţi (se scriu patru articole de câte doi octeţi). Fişierul este exploatat cu

171

Unele aspecte tehnice referitoare la prelucrarea fişierelor

articole de 6 octeţi (REAL), FileSize(f2) returnând valoarea 1 (8 DIV 6). Prima citire din fişier transferă primii 6 octeţi. A doua citire produce eroare de I/E (8 MOD 6 = 2). PROGRAM Ex8A; VAR f1:FILE OF WORD; x:WORD; f2:FILE OF REAL; y:REAL; BEGIN Assign(f1,'TEST.DAT'); Rewrite(f1); x:=0; Write(f1,x,x,x,x); Close(f1); Assign(f2,'TEST.DAT'); Reset(f2); Writeln(FileSize(f2)); Read(f2,y); Read(f2,y); {Error 100: Disk read error} Close(f2); END.

Este interesant de analizat şi modul în care lucrează funcţia Eof(f), în situaţia fişierului anterior. Dacă, după prima citire (6 octeţi), se apelează funcţia Eof(f2), aceasta returnează valoarea FALSE (programul de mai jos afişează textul FALSE), cu toate că pointerul este pe articolul cu numărul relativ FileSize(f2) (FileSize(f2)= =lfis DIV lart = 1). PROGRAM EX8B; VAR f1:FILE OF WORD; x:WORD; f2:FILE OF REAL; y:REAl; BEGIN Assign(f1,'TEST.DAT'); Rewrite(f1); x:=0; Write(f1,x,x,x,x); Close(f1); Assign(f2,'TEST.DAT'); Reset(f2); Read(f2,y); IF Eof(f2) THEN BEGIN Writeln('TRUE'); Readln END ELSE BEGIN Writeln('FALSE'); Readln END; Close(f2); END.

Din exemplul anterior se desprinde concluzia că funcţia Eof(f) returnează valoarea TRUE dacă pointerul este plasat după lfis octeţi faţă de începutul fişierului. Când nu se produce anomalie de lungime, afirmaţia anterioară coincide cu faptul că Eof(f) returnează valoarea TRUE când pointerul este pe articolul cu numărul relativ FileSize(f) (FileSize(f)*lart = lfis). De fapt, în cazul anomaliei de lungime, funcţia

172

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Eof(f) nu poate returna niciodată valoarea TRUE, deoarece "articolul scurt" nu poate fi niciodată citit. Din cele prezentate anterior, rezultă că, în cazul prelucrării unui fişier cu structură necunoscută, este recomandabil să se lucreze cu articole de tip CHAR sau cu blocuri de lungime unu.

173

OBIECTE ÎN PASCAL

Programarea orientată obiect (Object Oriented Programming - OOP) reprezintă o tehnică ce s-a impus în ultimii ani, dovedindu-se benefică pentru realizarea sistemelor software de mare complexitate. Noţiunea de obiect datează din anii ’60, o dată cu apariţia limbajului Simula. Există limbaje - ca Smalltalk şi Eiffel - care corespund natural cerinţelor programării orientate obiect, fiind concepute în acest spirit, alături de care o serie de alte limbaje procedurale, printre care C++ şi Turbo Pascal, au fost dezvoltate astfel încât să ofere suport pentru această tehnică. În prezent există în funcţiune sisteme software de mare anvergură realizate în tehnica programării orientată obiect, principiile ei fiind suficient de bine clarificate, astfel încât să se treacă din domeniul cercetării în cel al producţiei curente de programe.

10.1 Modelul de date orientat pe obiecte OOP reprezintă o abordare cu totul diferită faţă de programarea funcţională, devenită deja “clasică”. Dacă în programarea clasică programatorul era preocupat să răspundă la întrebarea “ce trebuie făcut cu datele?”, adică să definească proceduri care să transforme datele în rezultate, în OOP accentul cade asupra datelor şi legăturilor care există între acestea, ca elemente prin care se modelează obiectele lumii reale. Se poate afirma, într-o primă analiză, că OOP organizează un program ca o colecţie de obiecte, modelate prin date şi legături specifice, care interacţionează dinamic, adică manifestă un anumit “comportament”, producând rezultatul scontat. În general, pentru modelul de date orientat pe obiect, se consideră definitorii următoarele concepte: obiect, încapsulare, moştenire şi polimorfism. 1. Obiectul este modelul informaţional al unei entităţi reale, care posedă, la un anumit nivel, o mulţime de proprietăţi şi care are, în timp, un anumit comportament, adică manifestă o reacţie specifică în relaţiile sale cu alte obiecte din mediul său de existenţă. Ca model, un obiect este o unitate individualizabilă prin nume, care conţine o mulţime de date, proceduri şi funcţii. Datele descriu

174

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

proprietăţile şi nivelul acestora, iar funcţiile şi procedurile definesc comportamentul. Având în vedere proprietăţile comune şi comportamentul similar ale entităţilor pe care le modelează, obiectele pot fi clasificate, adică împărţite în mulţimi. O mulţime de obiecte de acelaşi fel constituie o clasă, care poate fi descrisă prin modelul comun al obiectelor sale. De exemplu, în figura 10.1, numerele raţionale, ca perechi de numere întregi de forma (Numarator, Numitor) pot fi descrise printr-un model comun, denumit ClasaRational. Modelul arată că orice obiect de acest fel se caracterizează printr-o pereche de numere întregi şi că pe această mulţime sunt definite operaţii unare şi binare, care arată cum “interacţionează” obiectele în interiorul mulţimii: un număr raţional poate da naştere opusului şi inversului său, două numere raţionale pot produce un alt număr raţional ca sumă, diferenţă etc. ClasaRational Numarator: Integer Numitor: Integer AddRational (b,c) SubRational (b,c) MulRational (b,c) DivRational (b,c) OpusRational (aopus) InversRational (ainvers)

Obiecte a: (5;7) b: (3;1) c: (2;5) d: (7;5) x: (0;1)

Fig. 10.1 Clasă şi obiecte – mulţimea numerelor raţionale Numerele complexe (figura 10.2), descrise ca perechi de numere reale de forma (p_reală, p_imaginară) pot fi grupate într-un model comun, denumit Ccomplexe: CComplexe p_reala:real; p_imaginara:real; Conjugat(b) Suma(b,c) Inmultire(b,c) Modul:real; Diferenta(b,c) Impartire(b,c)

Exemplu de obiect a:(2, 7.5) a reprezintă numărul complex 2+7.5i

Fig. 10.2 Clasă şi obiecte – mulţimea numerelor complexe

175

Obiecte în Pascal

Generalizând, se poate afirma că o clasă de obiecte se manifestă ca un tip obiect, iar modelul comun al obiectelor este modelul de definire a tipului obiect. Astfel, obiectele individuale apar ca manifestări, realizări sau instanţieri ale clasei, adică exemplare particulare generate după modelul dat de tipul obiect. Altfel spus, o clasă poate fi considerată ca un tip special de dată, iar obiectele sale ca date de acest tip. Acceptarea acestei semnificaţii pentru clase de obiecte este de natură să simplifice descrierea obiectelor şi să asigure un tratament al acestora similar tipurilor structurate de date din limbajele de programare: este suficientă o descriere a tipului obiect şi apoi se pot declara constante şi variabile de acest tip. Datele care reprezintă proprietăţile obiectelor se numesc atribute şi sunt de un anumit tip, de exemplu, întregi, real, boolean etc. Procedurile şi funcţiile care definesc comportamentul obiectelor sunt cunoscute ca metode ale clasei. Împreună, atributele şi metodele sunt membrii clasei, identificabili prin nume. Pentru a pune în evidenţă faptul că un membru aparţine unui obiect se utilizează calificarea (notaţia cu punct): nume_obiect.nume_membru. În figura 10.1, a.Numarator referă valoarea 5, iar a.AddRational(b,x) referă metoda AddRational a obiectului a pentru a produce obiectul rezultat x= a+b. Aşa cum sugerează figura 10.1, obiectele trebuie să conţină valorile lor pentru atribute, deoarece definesc starea obiectului respectiv. Metodele, fiind comune, se specifică o singură dată. Despre o metodă, desemnată sau apelată cu un anumit obiect, se spune că se execută în context obiect, iar obiectul respectiv este numit curent. Apelul propriu-zis este considerat ca trimitere de mesaj la obiectul curent, iar execuţia metodei reprezintă răspunsul obiectului curent la mesaj. Faptul că o metodă se execută în contextul obiectului curent înseamnă că are, în mod implicit, acces la atributele şi metodele obiectului, altele decât metoda respectivă. Pentru alte obiecte, din aceeaşi clasă sau din clase diferite, metoda trebuie să posede parametrii corespunzători. De asemenea, pentru a simplifica scrierea, în interiorul unei metode referirea la membrii obiectului curent se face fără calificare. Pe baza acestor convenţii, în procedurile AddRational şi OpusRational, scrise în pseudocod, s-a specificat cu un parametru mai puţin decât numărul de operanzi pe care îi presupune operaţia respectivă, deoarece un operand este obiectul curent. Referirea la obiectul curent se distinge de celelalte prin lipsa calificării. Procedure AddRational (b,c); c.Numărător:= Numarator * b.Numitor + Numitor * b. Numarator; c.Numitor: = Numitor * b.Numitor; End; Procedure OpusRational (aopus); aopus.Numarator: = -Numarator; aopus.Numitor: = Numitor; End;

176

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Descrierea în pseudocod a metodelor Conjugat, Suma şi Modul din clasa Ccomplexe (figura 10.2) poate fi făcută astfel: procedure Conjugat(b); begin b.p_reala:=p_reala; b.p_imaginara:=-p_imaginara; end; procedure Suma(b,c); begin c.p_reala:=p_reala+b.p_reala; c.p_imaginara:=-p_imaginara+b.p_imaginara; end; function Modul:real; begin Modul:=sqrt(sqr(p_reala)+sqr(p_imaginra)); end; Deoarece o clasă este un tip de dată, în definirea unei clase B se pot declara atribute de tip A, unde A este la rândul ei o clasă. Mai mult, o clasă A poate defini atribute de tip A. De exemplu, clasa Carte din figura 10.3 are atributul Autor de tipul Persoana care este, de asemenea, o clasă. Mai mult, Persoana are atributul Sef, care este de acelaşi tip (Persoana). Persoana 1 100 Ionescu Scriitor Persoana 2

Persoana Marca: Integer Nume: String Profesia: String Sef: Persoana

Carte Cota: String Titlu: String Autor:Persoana Pret: Real

Persoana 2 70 Popescu Reporter ---------Fig. 10.3 Atribute de tip Definirea atributelor unei clase ca tipuri ale altei clase pune în evidenţă o relaţie între clase şi, deci, între obiectele acestora. Din punct de vedere funcţional, metodele unei clase au destinaţii diverse. În multe cazuri şi depinzând de limbaj, unei clase i se poate defini o metodă constructor şi o metodă destructor. Un constructor este o metodă care creează un

177

Obiecte în Pascal

obiect, în sensul că îi alocă spaţiu şi/sau iniţializează atributele acestuia. Destructorul este o metodă care încheie ciclul de viaţă al unui obiect, eliberând spaţiul pe care acesta l-a ocupat. 2. Încapsularea exprimă proprietatea de opacitate a obiectelor cu privire la structura lor internă şi la modul de implementare a metodelor. Ea este legată de securitatea programării, furnizând un mecanism care asigură accesul controlat la starea şi funcţionalitatea obiectelor. Se evită astfel modificări ale atributelor obiectelor şi transformări ale acestora care pot să le “deterioreze”. Potrivit acestui mecanism, o clasă trebuie să aibă membrii împărţiţi în două secţiuni: partea publică şi partea privată. Partea publică este constituită din membri (atribute şi metode) pe care obiectele le oferă spre utilizare altor obiecte. Ea este interfaţa obiectelor clasei respective cu “lumea exterioară” şi depinde de proiectantul clasei. Modalitatea extremă de constituire a interfeţei este aceea a unei interfeţe compusă numai din metode. Dacă se doreşte ca utilizatorii obiectelor clasei să poată prelua şi/sau stabili valorile unor atribute ale acestora, interfaţa trebuie să prevadă metode speciale, numite accesorii. Partea privată cuprinde membri (atribute şi/sau metode) care servesc exclusiv obiectelor clasei respective. De regulă, în această parte se includ atribute şi metode care facilitează implementarea interfeţei. De exemplu, o stivă, ca tip de dată, poate fi descrisă de o clasă stack în care interfaţa este constituită din metodele Push, Pop, Top, Empty, în timp ce pointerul la capul stivei, Cap şi numărătorul de noduri, Contor, ca atribute, sunt “ascunse” în partea privată. Ea se serveşte de obiectele altei clase, denumită Nod, ale cărei obiecte le înlănţuieşte în stivă (figura 10.4). Trebuie remarcat că încapsularea înseamnă şi faptul că utilizatorul metodelor nu trebuie să cunoască codul metodelor şi nici nu trebuie să fie dependent de eventuala schimbare a acestuia, interfaţa fiind aceea care îi oferă funcţionalitate obiectelor în condiţii neschimbate de apelare. Stack Cap: Nod Contor: Integer Push ( ) Pop ( ) Top ( ) Empty ( )

Partea privată Interfaţa(partea publică)

Fig. 10.4 Interfaţa obiectelor 3. Moştenirea reprezintă o relaţie între clase şi este, probabil, elementul definitoriu al OOP. Relaţia permite constituirea unei noi clase, numită derivată, pornind de la clase existente, denumite de bază. Dacă în procesul de construire participă o singură clasă de bază, moştenirea este simplă, altfel este multiplă. În

178

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

continuare se vor aborda câteva aspecte legate de moştenirea simplă, singura implementată în Pascal. Se spune că o clasă D moşteneşte o clasă A, dacă obiectele din clasa D conţin toate atributele clasei A şi au acces la toate metodele acestei clase. Din această definiţie, dacă D moşteneşte A, atunci obiectele din D vor avea toate atributele şi acces la toate metodele lui A, dar în plus: - D poate defini noi atribute şi metode; - D poate redefini metode ale clasei de bază; - metodele noi şi cele redefinite au acces la toate atributele dobândite sau definite. În figura 10.5, clasa Cerc moşteneşte clasa Point, deci un obiect de tipul Cerc va avea ca membri coordonatele x,y moştenite şi ca atribut propriu Raza. Funcţia Distanţa, definită pentru calculul distanţei dintre punctul curent şi punctul p, dat ca parametru, este accesibilă şi pentru obiectele Cerc şi va calcula distanţa dintre două cercuri (distanţa dintre centrele lor). Funcţia Arie şi procedura Desenează sunt redeclarate de clasa Cerc, ceea ce înseamnă redefinirea lor impusă de codul diferit pe care trebuie să-l aibă în funcţie de tipul figurilor geometrice (cerc sau altă figură). Point x: Integer y: Integer Desenează ( ) Distanţa (p:Point): Real

Cerc Raza: Integer Arie ( ): Real Desenează ( )

x: 30 y: 150

x: 200 y: 180 Raza: 50

Fig. 10.5 Moştenirea simplă Dacă se au în vedere mulţimi de clase, atunci se observă că relaţia de moştenire simplă induce un arbore ierarhic de moştenire pe această mulţime. Există o singură clasă iniţială, şi anume rădăcina arborelui, fiecare clasă are un singur ascendent (părinte) şi orice clasă care nu este frunză poate avea unul sau mai mulţi descendenţi (fii). În fine, cu privire la moştenirea simplă se pot face următoarele observaţii: • dacă se aduc modificări în clasa de bază, prin adăugarea de atribute şi/sau metode, nu este necesar să se modifice şi clasa derivată; • moştenirea permite specializarea şi îmbogăţirea claselor, ceea ce înseamnă că, prin redefinire şi adăugare de noi membri, clasa derivată are, în parte, funcţionalitatea clasei de bază,

179

Obiecte în Pascal

la care se adaugă elemente funcţionale noi; • moştenirea este mecanismul prin care se asigură reutilizarea codului, sporind productivitatea muncii de programare. 4. Polimorfismul este un concept mai vechi al programării, cu diferite implementări în limbajele de programare care se bazează pe tipuri de date (limbaje cu tip). Ea şi-a găsit extensia naturală şi în modelul orientat pe date, implementat prin limbaje cu tip, în care clasa reprezintă tipul de date obiect. • Polimorfismul în limbajele de programare cu tip. Noţiunea de polimorfism exprimă capacitatea unui limbaj de programare cu tip de a exprima comportamentul unei proceduri independent de natura (tipul) parametrilor săi. De exemplu, o procedură care determină cea mai mare valoare dintr-un şir de valori este polimorfică dacă poate fi scrisă independent de tipul acestor valori. În funcţie de modul de implementare, se disting mai multe tipuri de polimorfism. Polimorfismul ad-hoc se materializează sub forma unor proceduri care au toate acelaşi nume, dar se disting prin numărul şi/sau tipul parametrilor. Polimorfismul este denumit şi supraîncărcare, având în vedere semantica specifică fiecărei proceduri în parte. Polimorfismul de incluziune se bazează pe o relaţie de ordine parţială între tipurile de date, denumită relaţie de incluziune sau inferioritate. Dacă un tip A este inclus (inferior) într-un tip B, atunci se poate pasa un parametru de tip A la o procedură care aşteaptă un parametru de tip B. Astfel, o singură procedură defineşte funcţional o familie de proceduri pentru toate tipurile inferioare celor declarate ca parametri. Un exemplu clasic este cazul tipului întreg, inferior tipului real în toate operaţiile de calcul. Polimorfismul parametric constă în definirea unui model de procedură pentru care înseşi tipurile sunt parametri. Polimorfismul, denumit şi genericitate, presupune că procedura se generează pentru fiecare tip transmis la apel ca parametru. Cele trei tipuri de polimorfism există (toate sau numai o parte din ele) în limbajele clasice de programare, dar unele pot să nu fie accesibile programatorului. Aşa este cazul limbajului Pascal în care polimorfismul ad-hoc este implicit numai pentru operatorii limbajului (+, -, /, * etc), polimorfismul parametric nu este implementat, iar polimorfismul de incluziune este aplicabil numai funcţiilor şi procedurilor de sistem. • Polimorfismul în limbajele orientate obiect. Limbajele orientate obiect sau extensiile obiect ale unor limbaje cu tip oferă, în mod natural, polimorfismul ad-hoc şi de incluziune. Polimorfismul ad-hoc intrinsec reprezintă posibilitatea de a defini în două clase independente metode cu acelaşi nume, cu parametri identici sau diferiţi. Acest polimorfism nu necesită mecanisme speciale şi decurge simplu, din faptul că fiecare obiect este responsabil de tratarea mesajelor pe care le primeşte. Polimorfismul este de aceeaşi natură şi în cazul în care între clase există o relaţie de moştenire, cu precizarea că, în cazul în care o metodă din clasa derivată are parametrii identici cu ai metodei cu acelaşi nume din clasa de bază, nu mai este

180

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

supraîncărcare, ci redefinire, după cum s-a precizat în paragraful anterior. Polimorfismul de incluziune este legat de relaţia de moştenire şi de aceea se numeşte polimorfism de moştenire. Într-adevăr, relaţia de moştenire este de ordine parţială. Când clasa D moşteneşte direct sau indirect clasa A, atunci D este inferior lui A. În aceste condiţii, orice metodă a lui A este aplicabilă la obiectele de clasă D şi orice metodă, indiferent de context, care are definit un parametru de tip A (părinte) poate primi ca argument corespunzător (parametru actual) un obiect de clasă D (fiu). Observaţie: un obiect de clasă A nu poate lua locul unui obiect de clasă D, deoarece A “acoperă” numai parţial pe D, care este o extensie şi o specializare a lui A. Limbajul Turbo Pascal, ca suport pentru programarea obiect, oferă ambele forme de polimorfism pentru clase. • Legarea statică şi dinamică a metodelor. Legarea statică a metodelor se regăseşte atât în limbajele orientate obiect cât şi în cele clasice. Compilatorul poate determina care metodă şi din care clasă este efectiv apelată într-un anumit context şi poate genera codul de apel corespunzător. În plus, datorită polimorfismului şi lucrului cu pointeri, în limbajele orientate obiect, unui obiect din clasa părinte, desemnat indirect prin referinţă (pointer) şi nu prin nume, i se poate atribui un obiect fiu. În general, nu este posibil de determinat dacă, în contextul dat, metoda polimorfică trebuie apelată în varianta clasei de bază sau a celei derivate. De aceea, compilatorul generează un cod care, la momentul execuţiei, va testa tipul efectiv al obiectului şi va realiza legarea metodei adecvate. În acest caz legarea este dinamică (sau la momentul execuţiei). Legarea dinamică este mai costisitoare decât cea statică, dar reprezintă o necesitate pentru a asigura elasticitatea necesară în realizarea programelor OOP, obiectele putând avea caracter de variabile dinamice.

10.2 Clase şi obiecte în Pascal Turbo Pascal implementează tehnica OOP pe fondul caracteristicilor sale de limbaj procedural, ceea ce înseamnă că lucrul cu clase şi obiecte se realizează asemănător cu cel cu tipuri structurate de date, mecanismul de încapsulare fiind asigurat prin construirea unităţilor.

10.2.1 Specificarea claselor În Pascal, clasele sunt tratate asemănător datelor de tip articol. Ţinând seama de faptul că o clasă conţine atât date cât şi metode, specificarea unei clase presupune declararea structurii şi definirea metodelor.

181

Obiecte în Pascal

• Declararea structurii. Declararea unei clase, ca tip, se face în secţiunea TYPE sub următoarea formă generală: referinta_la_clasa = ^nume_clasa {optional} nume_clasa=OBJECT atribut_1; ............... atribut_n; metoda_1; ............... metoda_m; { PRIVATE atribut_1; ............... atribut_p; metoda_1; ............... metoda_q; } END; Fiecare declaraţie de atribut are forma: nume_atribut:tip, unde tip poate fi predefinit sau definit anterior de utilizator, inclusiv numele unui alt tip de clasă. La fel ca şi la tipul RECORD, unele atribute pot fi referinţe, chiar la tipul clasei care se defineşte, caz în care declararea tipului referinţă aferent trebuie să preceadă declararea clasei. O declaraţie de metodă cuprinde numai antetul unei funcţii sau proceduri (signatura), adică numele, parametrii formali (dacă există) şi tipul rezultatului, dacă este cazul. Apariţia opţională a secţiunii PRIVATE în declaraţia de clasă anunţă partea privată a clasei. Toate atributele şi metodele care preced această secţiune sunt considerate publice. În fine, se remarcă prezenţa cuvântului OBJECT ca declarator de tip obiect. În Turbo Pascal nu există un declarator CLASS, pentru a introduce o clasă, aşa cum există, de exemplu, în C++. Deci declaratorul, OBJECT, nu declară un obiect, ci o clasă de obiecte. • Definirea. Definirea metodelor constă în construirea corpurilor subprogramelor a căror signatură a fost precizată. Deoarece într-un program pot să existe mai multe clase, definirea trebuie să asigure recunoaşterea metodelor diferitelor clase. Acest lucru se realizează prin prefixarea numelui metodei cu numele clasei. În rest, corpul metodelor se scrie în conformitate cu regulile generale ale limbajului şi având în vedere că metoda respectivă se execută în contextul obiectului curent.

182

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Exemplu: 10.1. Declararea şi definirea unei clase { Declararea clasei Point} Point = OBJECT Function Getx:Integer; Function Gety:Integer; Procedure Init (a, b: Integer); Function Distance (p: Point): Real; Private x: Integer; y: Integer; END; { Definirea metodelor} Function Point.Getx: Integer; Begin Getx: = x; End; Function Point.Gety: Integer; Begin Gety: = y; End: Function Point.Distance (p:Point): Real; Var dx, dy: Integer; Begin dx: = x-p.x; dy: = y-p.y; Distance: = sqrt(dx*dx-dy*dy); End; Procedure Point.Init(a, b: Integer); Begin x: = a; y: = b; End;

Din exemplul, 10.1 se observă modul de declarare a unei clase, în care, pentru asigurarea încapsulării depline, comunicarea cu exteriorul se face numai prin interfaţă. Funcţiile Getx şi Gety sunt necesare ca accesorii pentru a returna valorile atributelor x şi y, iar procedura Init este un constructor care iniţializează (schimbă) valorile celor două atribute. Funcţia Distance ilustrează modul în care trebuie înţeleasă afirmaţia că o metodă se execută în context obiect. Se observă că, deşi calculul distanţei presupune două obiecte de tip Point, ca parametru figurează numai un punct, p. Unde este celălalt (primul)? În mod implicit, se consideră că primul punct este un obiect ascuns, obiectul de apel al funcţiei, numit curent, coordonatele sale fiind x şi y. În relaţiile de calcul, referirea coordonatelor celuilalt punct, transmis ca argument, se va face prin notaţia cu calificări: p.x, p.y. Rezultă de aici că funcţiile Getx, Gety şi procedura Init se referă la obiectul curent.

183

Obiecte în Pascal

• Unit de clasă. Deoarece clasele trebuie să fie prin definiţie entităţi reutilizabile, se încapsulează într-o unitate. Declararea claselor apare în secţiunea INTERFACE, iar definirea metodelor în secţiunea IMPLEMENTATION. Se recomandă ca fiecare clasă să fie o unitate.

10.2.2 Utilizarea obiectelor Obiectele unei clase se comportă asemănător variabilelor de tip articol. Sintetizând aceasta, înseamnă că: • Obiectele se declară, ca şi variabilele, într-o secţiune VAR dintr-o entitate de program (program principal, funcţie, procedură sau unitate). De aici rezultă că obiectele pot avea caracter global sau local, ceea ce determină, pe de o parte, spaţiul de vizibilitate, iar, pe de altă parte, durata de viaţă şi segmentul de alocare. • Un obiect poate fi declarat static sau dinamic. Atributul de static, respectiv dinamic, se referă la momentul în care are loc alocarea spaţiului pentru obiect (la compilare sau la execuţie). Deoarece declararea obiectelor dinamice va fi tratată într-un alt paragraf, în continuare se prezintă numai aspectele privind obiectele statice. Pentru obiectele statice declaraţia are forma: nume_obiect:nume_clasa. De exemplu: p1, p2 : Point. Ca şi în cazul variabilelor, obiectele declarate trebuie să fie iniţializate înainte de a fi utilizate. Se apelează, în acest sens, o metodă de tip constructor. De exemplu, p2.Init (30,100) iniţializează punctul p2 cu x = 30 şi y = 100. Două obiecte de acelaşi tip pot participa într-o operaţie de atribuire. De exemplu, p1: = p2. Atribuirea între obiecte nu este întotdeauna corectă. Dacă obiectele au spaţiu extins, atunci copierea nu este corectă. De aceea, atribuirile trebuie utilizate cu multă atenţie, ele realizându-se prin copiere bit cu bit. Un obiect poate participa ca argument, prin valoare sau referinţă, la apelul de procedură sau funcţie. Deoarece transferul prin valoare corespunde unei copieri, pot apărea aceleaşi neajunsuri ca şi la operaţia de atribuire. În plus, copierea poate fi şi costisitoare ca spaţiu şi timp. De aceea se preferă transferul prin referinţă. O funcţie nu poate returna ca rezultat un obiect, dar poate returna o referinţă la un obiect. Orice metodă a unei clase, inclusiv constructorii, se apelează cu un anumit obiect care devine acti sau curent . Apelul are forma: obiect.nume_metoda(alte_argumente). De exemplu: pr. Init( 70,80); d:= p1.Distance (p2). Obiectul de apel este un parametru ascuns care se asociază automat cu un parametru formal denumit Self. Acest parametru formal prefixează, în mod

184

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

implicit, atributele şi metodele utilizate de metoda apelată. Se poate spune că Self este un pseudonim al obiectului de apel şi, de aceea, în metoda apelată se poate scrie: Self.atribut sau Self.metoda. Uneori Self se utilizează explicit pentru a evita ambiguitatea referirii sau atunci când este nevoie de a obţine adresa unui obiect curent. În ultimul caz, adresa se desemnează prin @Self. Exemplu: 10.2. Se declară şi se defineşte o clasă de numere raţionale ca perechi (Numarator, Numitor) de numere întregi. O astfel de clasă poate fi utilă în calcule cu fracţii ordinale, atunci când transformarea acestora în fracţii zecimale (numere reale) nu este de dorit din cauza erorilor de trunchiere. UNIT ClasaRat; INTERFACE Type Rational = OBJECT Procedure InitRat (x, y:Integer); Procedure AddRat (b: Rational; VAR c: Rational); Procedure SubRat (b: Rational; VAR c: Rational); Procedure MulRat (b: Rational; VAR c: Rational); Procedure DivRat (b: Rational; VAR c: Rational); Procedure OpusRat (VAR c: Rational); Procedure InversRat (VAR c: Rational); Function GetNumarator: Integer; Function GetNumitor: Integer; Procedure ReadRat; Procedure WriteRat; Function CompRat (b: Rational): Integer; PRIVATE Numarator, Numitor: Integer; Procedure SignRat; END; IMPLEMENTATION Function Cmmdc (x,y: Integer ):integer:FORWARD; Procedure Rational. InitRat (x, y: Integer); {Initializeaza in forma inductiva functia (numarator, numitor)} Var d: Integer; Begin If (x=0) or (y=1) then y=1 else begin If (abs(x)<>1) and (abs(y)<>1) then begin d:= Cmmdc (x,y) x:= x/d; y:= y/d; end; SignRat; End; Numarator:= x; Numitor:= y; End; Procedure Rational.AddRat (b: Rational; VAR c: Rational); Var x, y: Integer; Begin x: = Numarator*b.Numarator+Numitor*b.Numarator;

185

Obiecte în Pascal y: = Numitor*b.Numitor; c.InitRat (x, y); End; Procedure Rational.SubRat (b:Rational; VAR c:Rational); Var r: Rational; Begin b. OpusRat ( r ); AddRat (r, c); End; Procedure Rational.MulRat (b: Rational; VAR c: Rational); Var x, y: Integer; Begin x: = Numarator*b.Numarator; y: = Numitor*b.Numitor; c.InitRat (x, y); End; Procedure Rational.DivRat (b: Rational; VAR c: Rational); Var r: Rational; Begin b.InversRat ( r ); MulRat (r, c); End; Procedure Rational.InversRat (VAR c: Rational); Var d: Integer; Begin d:= Numarator; if d=0 then d:=1; c.Numarator: = c.Numitor; c.Numitor: = d; c.SignRat; End; Procedure Rational.OpusRat (VAR c: Rational); Begin c.Numarator: = - c.Numarator; End; Function Rational.GetNumarator: Integer; Begin GetNumarator: = Numarator; End; Function Rational.GetNumitor: Integer; Begin GetNumitor: = Numitor; End; Procedure Rational.SignRat; Begin if (Numarator>0) and (Numitor<0) or (Numarator<0) and (Numitor<0) then Begin x: = -x; y: = -y; End: End; Function Cmmdc (x, y: Integer) : Integer;

186

Programarea calculatoarelor – Tehnica programării în limbajul Pascal Begin x: = abs (x); y: = abs (y); While x< >y Do if x>y then x: = x-y else y: = y-x; Cmmdc: = x; End; Procedure Rational.ReadRat; Var txt: String [ 25]; i, x, y: Integer; Begin Read (txt); i: Pos (txt, ‘/’); If (i=0) then Begin {Numarator întreg} Val (txt, Numarator); Numitor: =1; End else Begin Val(Copy (txt, 1, C-1), x); Val (Copy (txt, i, 25), y); InitRat (x, y); End; End; Procedure Rational.WriteRat; Begin Write (Numarator, ‘/’, Numitor); End; Function Rational.CompRat (b: Rational): Integer; Var d, n1, n2: Integer; Begin d: = cmmdc (Numitor, b.Numitor); n1: = Numarator*(dDIVNumitor); n2: = b.Numarator*(dDIV b.Numitor); if (n1
Clasa construită poate fi testată printr-un program multifuncţional, TestE2. Operaţiile care se testează se codifică prin litere (A, S, M etc) la care se adaugă o operaţie, T, pentru terminare. Pe baza unei variabile Op (pentru operaţia curentă) se realizează o structură repetitivă, în corpul căreia se alege modul de tratare printro structură CASE OF. Program TestE3; Uses ClasRat; Var a,b,c: Rational; Op: Char; r: Integer; Procedure Meniu; Begin WriteLn (‘ Meniu’); WriteLn (‘A/a - Adunare’); WriteLn (‘S/s - Scadere’);

187

Obiecte în Pascal WriteLn (‘M/m - Inmulţire’); WriteLn (‘D/d - Impartire’); WriteLn (‘C/c - Comparare’); WriteLn (‘G/g - Elemente’); WriteLn (‘T/t - Terminare’); WriteLn (‘ Alege operatia:’); ReadLn ( Op ); Op: = UpCase ( Op ); End; BEGIN Meniu; While Op <>’T’ Do Begin If (Op = ‘G’) then Begin a.ReadRat; WriteLn (‘Numarator=’, a.GetNumarator); WriteLn (‘Numitor=’, a.GetNumitor); End else If (Op = ‘C’) then Case a.CompRat (b) of -1: WriteLn (‘ ab’); End else Begin CaseOp of ‘A’: a.AddRat (b,c); ‘S’: a.SubRat (b,c); ‘M’: a.MulRat (b,c); ‘D’: a.DivRat (b,c); End; C.WriteRat; End; Meniu; End; END.

La fel ca în exemplul 10.1, şi în acest caz atributele sunt private, iar accesul la valorile lor se asigură prin accesoriile GetNumarator, GetNumitor. În partea privată apare o metodă care este utilizată numai intern (în clasă), în scopul asocierii semnului numărului raţional la numărător. De asemenea, se remarcă prezenţa funcţiei Cmmdc (cel mai mare divizor comun) care nu ţine de clasă, dar este necesară implementării unor metode ale clasei. La stabilirea numărului de parametri, în conceperea metodelor s-a avut în vedere că un operand, eventual unicul pentru operaţii unare, este obiectul curent. Atunci când, în contextul obiectului curent, s-a apelat o metodă care se referă la un alt obiect, acesta este precizat explicit, (altfel s-ar considera acelaşi context). De exemplu, în operaţiile de scădere şi împărţire se aplică o reducere la operaţiile de adunare cu opusul, respectiv înmulţire cu inversul. Acest rezultat intermediar se obţine prin b.OpusRat(r), respectiv b.InversRat(r), după care, în contextul definit la apelul lui SubRat, respectiv DivRat se invocă AddRat(r,c) şi MulRat(r,c), ceea ce

188

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

înseamnă că ele vor fi de fapt apelate ca Self.AddRat(r,c) şi Self.MulRat(r,c), adică a va fi prim operand. O menţiune aparte trebuie făcută cu privire la constructorul InitRat care iniţializează în formă ireductibilă fracţia (Numarator, Numitor) a unui obiect. De aceea, ea este apelată ori de câte ori se creează o astfel de fracţie, fie în programul principal, fie într-o altă metodă. Metodele ReadRat şi WriteRat realizează legătura obiectelor cu tastatura şi monitorul. Procedura ReadRat oferă o alternativă de iniţializare a obiectelor, prin citirea fracţiilor de la tastatură sub forma x/y sau x, dacă numitorul este unu (număr întreg). Procedura WriteRat afişează pe monitor un număr raţional sub formă de fracţie x/y, în poziţia curentă de scriere. În cazul claselor numerice (dar nu numai), pe mulţimea obiectelor unei clase se poate defini o relaţie de ordine. Deoarece limbajul nu permite supraîncărcarea operatorilor, este necesar să se introducă o metodă, de regulă o funcţie, care să definească relaţia. Este cazul funcţiei CompRat care compară două numere raţionale a, b şi returnează -1, 0, 1, după cum între cele două numere există una din relaţiile: ab. Legat de precizările referitoare la supraîncărcarea operatorilor, se observă o lipsă de naturaleţe în scrierea operaţiilor definite pe mulţimea obiectelor. De exemplu, se scrie a.AddRat(b, c) în locul formei c:=a+b sau a.CompRat (b) în loc de a
10.2.3 Constructori şi destructori De cele mai multe ori, spaţiul unui obiect este compact, constituit din spaţiile necesare atributelor sale. În cazul în care unele atribute sunt de tip referinţă, spaţiul obiectelor clasei respective are o componentă dinamică denumită spaţiu extins (figura 10.6).

189

Obiecte în Pascal

Spaţiu compact (static) atribut _1 referinta _1 atribut _2 atribut _3 referinta _2

Spaţiu extins (dinamic)

Fig. 10.6 Spaţiul extins al unui obiect Atunci când se declară o variabilă obiect dintr-o astfel de clasă, compilatorul poate aloca spaţiul compact necesar. Spaţiul extins, la acest moment, este necunoscut deoarece el trebuie să fie alocat în zona memoriei dinamice, la momentul execuţiei. Rezultă de aici două aspecte importante pe care trebuie să le aibă în vedere proiectantul clasei. • În primul rând, trebuie să se prevadă un constructor capabil să trateze nu numai iniţializarea atributelor statice, dar şi a celor care ocupă un spaţiu dinamic. Constructorul trebuie să utilizeze procedura New sau GetMem pentru a obţine un bloc de mărimea necesară, a cărui adresă trebuie încărcată în câmpul referinţă al obiectului, iar blocul respectiv trebuie apoi iniţializat. Într-o clasă pot fi prevăzuţi mai mulţi constructori specializaţi, dacă există mai multe câmpuri de tip referinţă la un obiect, dacă un singur constructor ar fi incomod de utilizat sau ar avea o listă prea mare de parametri. Mai mult, limbajul prevede o posibilitate specială de a declara un constructor, utilizând declaratorul Constructor în loc de Procedure. Printr-o astfel de declarare, compilatorul are posibilitatea să distingă din mulţimea metodelor pe cele care au acest rol special, lucru necesar în cazul moştenirii, dar util şi programatorului pentru a putea verifica mai uşor dacă a rezolvat corect problema iniţializării obiectelor. • În alt doilea rând, atunci când un obiect trebuie să-şi încheie existenţa (de exemplu, la ieşirea dintr-o funcţie/procedură în care a fost creat), este necesar să se prevadă o metodă destructor care să elibereze spaţiul extins. Destructorul trebuie să apeleze procedura Dispose sau FreeMem pentru a returna acest spaţiu zonei heap. În lipsa unui destructor rămân blocuri de memorie ocupate, fără ca ele să mai poată fi referite ulterior. Declaraţia destructorului se poate face prin declaratorul Destructor în loc de Procedure. Constructorii şi destructorii pot avea orice utilitate pe care o consideră programatorul, atunci când se creează sau se distrug obiecte. Atribuirea între obiectele care au spaţiu extins are unele particularităţi. Astfel, dacă se specifică atribuirea a:=b, atunci se realizează o copiere bit cu bit a obiectului b în a. După operaţie, a şi b au acelaşi conţinut în spaţiul compact şi, deci, referă acelaşi spaţiu extins (figura 10.7).

190

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

x y z

a:=b

. . .

x y z

. . . Referinţă

Referinţă Spaţiu extins

Fig. 10.7 Atribuirea în cazul obiectelor cu spaţiu extins

Ulterior, modificarea realizată la unul din obiecte în spaţiul extins afectează starea şi pentru celălalt obiect. Dacă un obiect dispare, atunci celălalt obiect va rămâne fără spaţiu extins, iar când şi acesta va dispărea, destructorul său va încerca să elibereze un spaţiu care nu mai există, fiind deja eliberat. În legătură cu astfel de cazuri, deoarece operatorul de atribuire nu poate fi supraîncărcat, este necesar să se evite operaţia şi să se prevadă o metodă proprie de copiere prin care să se iniţializeze a cu valorile lui b, dar în spaţiu complet separat. De asemenea, se recomandă ca obiectele de acest fel, utilizate ca argumente, să fie transmise, prin referinţă şi nu prin valoare, având în vedere că ultima metodă este echivalentă cu o atribuire. Exemplu: 10.3. Se ilustrează modul de utilizare a referinţelor în declararea claselor, de realizare a constructorilor multipli şi a destructorului de spaţiu extins. Deşi simplificat, exemplul sugerează modul în care se poate construi o clasă matrice cu elemente de tip Rational, prin utilizarea clasei din exemplul 10.2. UNIT ClasaMRat; INTERFACE uses ClasaRat; Const MaxLin = 10; MaxCol = MaxLin; ZeroRat : Rational = (Numarator:0; Numitor:1); Type DimLin = 1..MaxLin; DimCol = 1..MaxCol; Matrice = array [DimLin, DimCol] of Rational; MatriceRat = OBJECT Constructor SetNulMat (m, n:Integer); Constructor ReadMat; Procedure AddMat (b:MatriceRat; VAR c:MatriceRat); Procedure WriteMat; Destructor FreeExtMat; PRIVATE

191

Obiecte în Pascal Nlin, Ncol:Integer; PMat:^Matrice; END; Var ErrMat:Integer; IMPLEMENTATION Constructor MatriceRat.SetNulMat (m, n:Integer); Var i, j:Integer; Begin m:=Max (m, MaxLin); n:=Max (n, MaxCol); GetMem (Pmat, m*n*SizeOf (Rational)); For i:=1 to m Do For j:=1 to n Do P.Mat^[i, j]:=ZeroRat; Nlin:= m; Ncol:=n; End; Constructor MatriceRat.ReadMat; Var m, n, i, j:Integer; Begin Repeat Write (‘Numărul de linii:’); ReadLn (m); Until m<=MaxLin; Repeat Write (‘Numărul de coloane:’); ReadLn (n); Until n<=MaxCol; WriteLn(‘Elementele rationale ale matricei, linii!’); GetMem (Pmat, m,*n*SizeOf (Rational)); For i:=1 To m Do For j:=1 To n Do Begin Write (‘x(‘, i:2,’,’,j:2,’)=’); PMat^[i,j].ReadRat; WriteLn; End; Nlin:=m; Ncol:=n; End;

pe

Destructor MatriceRat.FreeExtMat; Begin FreeMem (PMat, Nlin*Ncol*SizeOf (Rational)); End; Procedure MatriceRat.AddMat (b:MatriceRat; VAR c:MatriceRat); Var i, j:Integer; Begin ErrMat:=0; If (Nlin=b.Nlin) and (Ncol=b.Ncol) then For i:=1 To Nlin Do For j:=1 To Ncol Do PMat^[i,j].AddRat(b.PMat^[i,j],c.PMat^[i,j]); else ErrMat:=1; End; Procedure MatriceRat.WriteMat; Var i, j:Integer; Begin For i:=1 To Nlin Do

192

Programarea calculatoarelor – Tehnica programării în limbajul Pascal Begin WriteLn (‘Linia:’, i); For j:=1 To Ncol Do Begin PMat^[i, j].WriteRat; If (i<>Ncol) Write (‘;’); End; End; End; Program Test; Uses ClasMat; Var A,B,C:MatriceRat; Begin A.ReadMat; B.ReadMat; A.AddMat (B,C); If(ErrMat)then WriteLn diferite’) else C.WriteMat; A.FreeExtMat; B.FreeExtMat; C.FreeExtMat; ReadLn; END.

(‘**

Eroare:Dimensiuni

Analiza exemplului pune în evidenţă posibilităţile tehnicii OOP în limbajul Pascal. • Iniţializarea unor variabile de tip obiect, în secţiunea CONST. Este cazul variabilei ZeroRat, pentru care se observă tratarea iniţializării ca în cazul articolelor, definindu-se variabile pentru fiecare atribut, potrivit tipului său. • Definirea structurii de tip array, la care tipul de bază este un tip obiect. Tipul Rational permite definirea unui tip nou, Matrice, ca un masiv bidimensional. • Alocarea dinamică a spaţiului pentru matrice drept spaţiu extins, potrivit numărului de linii şi coloane, dar fără a se depăşi dimensiunile declarate ale tipului Matrice. Se elimină, astfel, risipa de spaţiu prin alocarea statică, la dimensiuni constante, pentru structura de tip masiv. De observat modul de referire a elementelor matricei şi utilizarea metodelor clasei Rational pentru a trata aceste elemente. Se spune că între clasa MatriceRat şi clasa Rational există o relaţie de tip client-server. • Prezenţa mai multor constructori şi a unui destructor pentru iniţializarea, inclusiv a spaţiului extins şi, respectiv, eliberarea acestuia. Destructorul este apelat la sfârşitul programului, pentru fiecare obiect. Spaţiul compact al obiectelor face obiectul eliberării numai în cazul obiectelor locale, la ieşirea din procedura sau funcţia în care obiectele au fost create, când este automat returnat stivei. • Tratarea erorilor clasei prin intermediul unei variabile publice (din interfaţă). Aceasta este utilizată în cazul operaţiei de adunare şi returnează valoarea unu dacă matricele nu au aceeaşi dimensiune, respectiv zero, în caz contrar. Este sarcina programului care apelează metodele clasei să verifice, la reîntoarcerea

193

Obiecte în Pascal

controlului, ce valoare are variabila publică. Variabila nu este legată de clasă, ci de unitatea în care este încorporată clasa.

10.2.4 Utilizarea obiectelor dinamice Obiectele statice nu sunt, în general, reprezentative pentru tehnica OOP. Cel mai frecvent, obiectele au un comportament dinamic, adică în timp se nasc, trăiesc şi dispar. În mod corespunzător, trebuie să existe posibilitatea gestionării dinamice a spaţiului lor, astfel încât, cu aceleaşi rezerve de memorie, să poată fi utilizate obiecte multiple, eventual de tipuri diferite. În limbajele orientate obiect, obiectele sunt, prin definiţie, dinamice şi sistemul preia sarcina de a aloca şi elibera spaţiul de memorie pentru acestea.Tehnica OOP implementată în Pascal face uz de facilităţile de definire a variabilelor dinamice, dar gestiunea obiectelor este lăsată exclusiv în sarcina programatorului. În aceste condiţii, declararea obiectelor dinamice se poate face în una din formele: nume_referinta_clasa : ^nume_clasa; nume_referinta_clasa : nume_tip_referinta; Prima formă corespunde declarării cu tip anonim a variabilelor şi este mai puţin indicată. A doua formă utilizează explicit un tip referinţă spre clasă şi este posibilă numai dacă un astfel de tip a fost declarat. De regulă, pentru a da posibilitatea definirii obiectelor dinamice, orice clasă se declară în forma: nume_tip-referinta : ^nume_clasa; nume_clasa =OBJECT.......END; Aici se face uz de excepţia introdusă de limbaj, de a declara o referinţă înainte de declararea tipului de date pe care îl referă. Această permisiune asigură referinţe în interiorul obiectelor, la obiecte din propria clasă şi posibilitatea practică de a construi liste înlănţuite de variabile şi obiecte. De exemplu, dacă se presupun declaraţii de tip de forma: PPoint=^TPoint; TPoint=OBJECT.....END; PRational=^TRational; TRational=OBJECT....END; atunci este posibil să se declare variabile obiect de forma: PtrP1,PtrP2 : Ppoint; PtrRat1: PRational; După o astfel de declarare, compilatorul alocă spaţiu (static) pentru variabilele referinţă. Este sarcina programatorului să aloce spaţiul dinamic şi să apeleze constructorii/ destructorii de iniţializare/eliberare. În acest scop se utilizează procedurile New şi Dispose, care au fost extinse astfel încât să accepte drept parametru şi apelul la un constructor sau destructor. Mai mult, procedurile posedă o replică sub formă de funcţie. Noua sintaxă a acestora se prezintă astfel:

194

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

New(variabila_referinta); New(variabila_referinta, apel_constructor); Variabila_referinta := New(tip_referinta, apel_constructor); Dispose(variabila_referinta) Dispose(varaibila_referinta, apel_constructor); De exemplu, dacă Init(x,y:Integer) este un constructor pentru cazul clasei Tpoint, se poate scrie: New (PtrP1); PtrP1^.Init (30,20); sau New (PtrP1, Init(30, 20)); sau PtrP1:=New (Ppoint, Init (30, 20)); În acest mod se asigură o posibilitate în plus pentru evitarea omisiunii apelului constructorului. Similar stau lucrurile şi cu eliberarea spaţiului. Dacă obiectele dinamice nu au spaţiu extins, atunci eliberarea spaţiului compact dinamic se va face prin apelul procedurii Dispose, în prima formă; de exemplu, Dispose (PtrP1). Dacă obiectul dinamic are spaţiul extins, atunci clasa prevede şi un destructor pentru acesta (destructor de spaţiu extins). În acest caz se poate utiliza procedura Dispose în a doua formă. De exemplu, dacă se presupune clasa MatriceRat (exemplul_3), declarată în forma: MatriceRat=^TMatriceRat; TMatriceRat= OBJECT…End; şi declaraţii de obiecte de forma: PtrA:TMatriceRat, se poate apela destructorul FreeExtMat în forma: Dispose (PtrA, FreeExtMat). Prin acest apel se execută mai întâi destructorul, deci are loc eliberarea spaţiului extins şi apoi se eliberează spaţiul compact dinamic. Referirea unui obiect dinamic are forma referinta^ şi accesul la membrii obiectelor va avea formele: referinta^.nume_metoda(argumente) referinta^.atribut Aşa cum se pot construi masive de obiecte statice, tot aşa se pot construi astfel de structuri având ca tip de bază referinţe la obiecte dinamice. Accesul la membrii obiectului k se va face sub forma: numearray[k]^.nume_metoda(argumente); numearray[k]^.atribut.

10.3 Moştenirea în Pascal Moştenirea este o relaţie de descendenţă ierarhică între clase, care oferă mecanismele necesare pentru ca o clasă derivată să dobândească atributele

195

Obiecte în Pascal

părinţilor şi să primească dreptul de acces la metodele acestora. Deoarece permite reutilizarea de cod, moştenirea este aspectul definitoriu al metodei OOP.

10.3.1 Aspecte de bază În limbajul Pascal, moştenirea este implementată sub forma simplă, ceea ce înseamnă că o clasă nouă poate fi derivată dintr-o singură clasă părinte (de bază). Pentru ca o clasă să fie recunoscută ca bază a unei clase noi, declaraţia acesteia trebuie să aibă forma: nume_clasa_derivata = OBJECT(nume_clasa_baza) declaratii membri; End; În Pascal, clasa derivată poate să definească membri noi sau să utilizeze supraîncărcarea unor metode ale părintelui. Dacă se are în vedere arborele de moştenire, clasa derivată poate supraîncărca metode ale oricărui ascendent. Totuşi, supraîncărcarea în Pascal are unele limite faţă de alte implementări. Operatorii limbajului nu pot fi supraîncărcaţi, iar o metodă nu poate fi supraîncărcată de mai multe ori în cadrul aceleiaşi clase. Din punct de vedere sintactic şi semantic, aceste limitări constituie neajunsuri, fiind necesară introducerea unor metode distincte pentru operaţii formal identice, dar care acţionează asupra unor obiecte de tipuri diferite. Dacă este necesar ca într-o clasă derivată, care supraîncarcă o metodă a unui strămoş, să fie referită explicit această metodă, atunci, sintactic, numele clasei strămoş trebuie să prefixeze numele metodei: nume_clasa_stramos.nume_metoda_supraincarcata( ) Dacă într-un arbore de moştenire o clasă nu posedă o anumită metodă, dar o referă fără a preciza clasa strămoş căreia îi aparţine, compilatorul caută ascendent până la prima apariţie a ei. Limbajul prevede, de asemenea, o regulă adecvată pentru asigurarea iniţializării corecte a obiectelor aparţinând unui arbore de derivare. Se cere ca fiecare clasă să aibă un constructor care să apeleze constructorul clasei părinte pentru partea moştenită şi apoi să iniţializeze atributele proprii (figura 10.9). O situaţie similară, dar reciprocă, este cea a destructorilor. Limbajul prevede că, în cazul obiectelor unei ierarhii, apelul ascendent al destructorilor este automat. Deci, dacă o clasă (finală) posedă un destructor pentru spaţiul extins, atunci execuţia acestuia declanşează procesul de eliberare, din aproape în aproape, a spaţiilor extinse moştenite de la toţi strămoşii, prin execuţia destructorilor acestora. Pentru ca mecanismul să funcţioneze corect şi în cazul în care există clase strămoş care nu posedă spaţiu extins, limbajul permite destructori cu corp vid, pe care compilatorul îi generează automat la clasele respective. Pentru mai multă claritate, este recomandabil ca programatorul să definească destructori cu corp vid pe ramurile care nu conţin cel puţin un destructor propriu-zis.

196

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

O menţiune aparte trebuie făcută pentru cazul obiectelor dinamice ale unei ierarhii de clase. Datorită polimorfismului, orice obiect este compatibil, la atribuire, cu oricare obiect al unui strămoş. În aceste condiţii, o referinţă la un obiect strămoş poate conţine adresa unui obiect descendent şi apelul unei metode, de forma: PtrStramos^.Numemetoda ( ), introducând un element de nedeterminare în privinţa obiectului de apel. Pentru astfel de cazuri, în limbajul Pascal s-a stabilit urmatoarea regulă care se aplică în lipsa altor informaţii adiţionale: se apelează metoda clasei corespunzăroare tipului referinţei, dacă o astfel de metodă există, sau metoda corespunzătoare primului ascendent care o posedă.

Moştenire

Clasa A

Clasa A

Clasa A

a

a b c

a b c d

Init

Init

Init

Fig. 10.9 Moştenirea şi apelul Exemplu: 10.4. În ramura TX←TY←TZ←TW, definită în continuare, apare o procedură supraîncărcată, p. PX=^TX; TX=OBJECT Procedure p END; PY=^TY; TY=OBJECT (TX) Procedure p END; PZ=^TZ; TZ=OBJECT (TY) Procedure q END; PW=^TW; TW=OBJECT (TZ) Procedure r END;

Dacă se declară obiectele dinamice PtrX:PX; PtrW:PW şi se face atribuirea şi apelul:

197

Obiecte în Pascal .................. PtrX:=PtrW; PtrX^.p;

se va apela procedura p a clasei TX, ghidându-se după referinţa PtrX care este asociată acestei clase. Dacă se declară PtrZ:PZ; PtrW:PW şi se fac atribuirea şi apelul: .................. PtrZ:=PtrW; PtrZ^.p;

atunci se va apela metoda p a clasei TY, primul ascendent al clasei TZ, deţinătoarea referinţei PtrZ, care posedă această metodă. Ipostazele incluse în compilator, după regula de mai sus, permit legarea statică a metodelor, care este simplă şi eficientă, dar care poate să nu fie convenabilă în multe cazuri.

10.3.2 Legarea dinamică. Metode virtuale Legarea dinamică se referă la metode supraîncărcate, identice ca prototip, care sunt declarate într-o ramură a unei ierarhii de clase. În acest caz particular de supraîncărcare, se spune că metoda este redefinită, având în vedere funcţionalitatea nouă (corpul nou) care i se asociază. Este de dorit ca metoda să fie apelată cu obiectul a cărui adresă o conţine referinţa de apel, fapt ce nu poate fi stabilit decât la momentul execuţiei. Devine evidentă necesitatea înştiinţării compilatorului asupra acestei intenţii, pentru a nu aplica regula legării statice şi, în consecinţă, pentru a genera codul necesar determinării obiectului real şi metodei de apelat la momentul execuţiei. O astfel de tehnică de legare a unei metode cu obiectul dinamic este denumită dinamică (late binding). Declararea unei metode din această categorie se realizează prin declaratorul VIRTUAL, inclus în linia de definire a prototipului: procedure nume (parametri); VIRTUAL; function nume (parametri): tip_rezultat; VIRTUAL; Metodele redefinite sunt denumite virtuale. Dacă o metodă a fost declarată virtuală într-o clasă, atunci trebuie să fie declarată la fel în toate clasele derivate care au această clasă la rădăcină şi redefinesc metoda în cauză. Totuşi, un descendent nu este obligat să redefinească o metodă virtuală a unui ascendent, subînţelegându-se, în acest caz, că se aplică regula căutării ascendente, dacă un obiect al său o referă. O menţiune aparte trebuie făcută cu privire la constructori şi destructori. În primul rând, trebuie remarcat faptul că obiectele care posedă metode virtuale trebuie să fie iniţializate prin constructor, dar constructorii nu pot fi declaraţi ca metode virtuale. Regula este de natură să asigure o corectă iniţializare, prin evitarea situaţiilor necontrolabile de iniţializare parţială, posibile în cazul legării dinamice. În al doilea rând, se menţionează, ca regulă, posibilitatea de a virtualiza destructorii. Spre deosebire de constructori, virtualizarea destructorilor este, în

198

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

multe cazuri, absolut necesară, deoarece se recomandă ca procedeu general. Ce se poate întâmpla în cazul în care destructorii nu sunt virtualizaţi rezultă şi din exemplul care urmează. Exemplu: 10.5. Se presupune derivarea TA←TB←TC şi destructorul virtual D. Se declară un masiv de referinţe la obiecte de clasă TA care se iniţializează cu adresele unor obiecte. Se execută apoi ştergerea obiectelor dinamice, cu apelul destructorului D, pentru a elibera spaţiul extins al acestor obiecte. PA = ^ TA; TA = OBJECT ........................................ Destructor D ; VIRTUAL; End; PB = ^ TB; TB = OBJECT (TA) ........................................ Destructor D ; VIRTUAL; End; PC = ^TC; TC = OBJECT (TB) ........................................ Destructor D; VIRTUAL; End; ............................................................. p:array [0..2] of PA; ............................................................. .. p[0]: = new (PB,…); {contine obiect TB} p[1]: = new (PC,…); {contine obiect TC} p[2]: = new (PA,…); {contine obiect TA} ............................................................. dispose (p[0], D); {apel destructor TB, TC} dispose (p[1], D); {apel destructor TC, TB, TA} dispose (p[2], D); {apel destructor TA}

Se observă că p conţine referinţe la diferite obiecte, în virtutea polimorfismului. Ştergerea obiectelor ale căror adrese sunt în componentele lui p implică o legare dinamică a destructorului potrivit. Astfel, pe baza regulii de apel automat al destructorilor în sens ascendent, se eliberează corect, în fiecare caz, spaţiul extins propriu şi cel moştenit. Dacă D nu ar fi fost declarat virtual, deoarece p are tipul de bază corespunzător referinţei la clasa TA, în fiecare caz s-ar fi apelat numai destructorul acestei clase, ceea ce ar fi însemnat neeliberarea întregului spaţiu extins al obiectelor respective. Mecanismul metodelor virtuale este implementat prin intermediul tabelei de metode virtuale VMT (Virtual Methods Table) a clasei care se ataşează părţii de date a obiectelor din acea clasă (figura 10.10).

199

Obiecte în Pascal

Obiect 1

Obiect 2

Atribut 1

Atribut 1

Atribut 2

. . .

VMT

Atribut 2

. . .

Date de control @Metodă virtuală 1

@VMT

@Metodă virtuală 2

@VMT

. . . Fig. 10.10 Tabela de metode virtuale

Tabela de metode virtuale este ataşată la obiect de unul din constructori, care este completat corespunzător de către compilator şi este apelat înainte de apelul oricărei metode virtuale a obiectului respectiv.

10.3.3 Moştenirea şi instanţele Un tip obiect descendent dintr-un altul moşteneşte, în particular, atributele ascendentului. El posedă, deci, cel puţin câmpurile ascendentului, plus, eventual, altele noi. O instanţă a unui tip ascendent poate fi, deci, atribuită cu o instanţă a descendentului (obiect_parinte:=obiect_fiu). O eventuală atribuire inversă este incorectă, pentru că ar avea ca efect neiniţializarea atributelor noi ale descendentului. De asemenea, în cazul unui transfer prin parametri valoare, o instanţă părinte poate fi substituită cu una fiu. Dacă, în schimb, obiectele conţin spaţiu extins şi unuia dintre obiecte îi este disponibilizat spaţiul extins, atunci şi celălalt îşi va pierde spaţiul extins şi, dacă el îşi va apela destructorul, acesta va încerca eliberarea unui spaţiu de memorie inexistent. De asemenea, dacă tipurile de obiecte conţin metode virtuale, o atribuire nu este în general suficientă în iniţializarea corectă a unei instanţe. O instrucţiune de atribuire utilizează dimensiunea unei instanţe pentru transferul de date. În cazul claselor ce conţin metode virtuale, dimensiunea este conţinută într-un câmp al VMT, deci poate fi cunoscută numai după apelul constructorului respectiv. Efectul unei instrucţiuni de atribuire este, deci, consistent doar în funcţie de dimensiuni, acestea putând fi determinate numai după apelul constructorului.

200

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Exemplu: 10.6. Lucrul cu metode virtuale program virt; uses crt; type parinte=object a,b:word; constructor init_p(a1,b1:word); function suma:word;virtual; end; fiu=object(parinte) c:word; constructor init_f(a1,b1,c1:word); function suma:word;virtual; end; var p:parinte; f:fiu; constructor parinte.init_p; begin a:=a1; b:=b1; end; function parinte.suma:word; begin suma:=a+b; end; constructor fiu.init_f; begin a:=a1; b:=b1; c:=c1; end; function fiu.suma:word; begin suma:=a+b+c; end; procedure afis; begin writeln('Instanta parinte ',sizeof(p)); writeln('Instanta fiu dimensiunea ', sizeof(f)); readln; end;

a=',p.a,' a=',f.a,'

b=',p.b,

'

dimensiunea

b=',f.b,'

c=',f.c,'

begin clrscr; fillchar(p,sizeof(parinte)+sizeof(fiu),#0); afis; {1} f.init_f(1,2,3); afis;{2} p:=f; afis;{3} p.init_p(4,5); afis;{4} p:=f; afis;{5} end.

201

Obiecte în Pascal

Rezultatul execuţiei programului {1} parinte fiu {2} parinte fiu {3} parinte fiu {4} parinte fiu {5} parinte fiu

a=0 a=0 a=0 a=1 a=0 a=1 a=4 a=1 a=1 a=1

b=0 b=0 b=0 b=2 b=0 b=2 b=5 b=2 b=2 b=2

c=0 c=3 c=3 c=3 c=3

dimensiune 0 dimensiune 0 dimensiune 0 dimensiune 8 dimensiune 0 dimensiune 8 dimensiune 6 dimensiune 8 dimensiune 6 dimensiune 8

10.4 Proiectarea ierarhiilor de clase Tehnica OOP se bazează pe date şi pe clasificarea corespunzătoare a acestora. De aceea, pentru a asigura reutilizarea de cod, obiectivul principal al proiectării este construirea unui arbore de moştenire cât mai judicios. În continuare se vor face câteva recomandări de proiectare, ilustrate pe baza unui exemplu didactic de obiecte grafice, redat în detaliu la sfârşitul capitolului.

10.4.1 Clase abstracte Dacă în procesul de analiză se privilegieză datele, atunci primul lucru care trebuie pus în evidenţă sunt mulţimile de obiecte care trebuie tratate. Pentru cazul obiectelor vizuale, acestea sunt: puncte, linii, dreptunghiuri, cercuri etc. În continuare trebuie căutate acele date (caracteristici) care sunt definitorii pentru diferitele tipuri de obiecte şi trebuie puse în evidenţă acele elemente care sunt comune. Datele comune se constituie în clase şi reprezintă acele date care pot fi moştenite. În cazul figurilor geometrice, se poate constata că orice obiect se identifică printr-un punct de cordonate (x,y) reprezentând începutul liniei, colţul stânga sus al dreptunghiului, centrul cercului etc, că orice figură presupune o curbă de contur trasată cu o anumită culoare, că la figurile închise se pune în evidenţă o suprafaţă care poate fi umplută (haşurată şi colorată) într-un anumit mod etc. Se poate uşor constata că, aplicând principiul grupării datelor comune în clase, se ajunge în situaţia de a obţine clase pentru care nu are sens să se declare obiecte. Astfel, de exemplu, ar fi clasa care descrie curba de contur a obiectelor (Contur) sau cea care conţine elementele definitorii pentru suprafaţa interioară a obiectelor (Suprafaţa). Astfel de clase se numesc abstracte (figura 10.11). Ele nu pot fi instanţiate, adică nu pot genera obiecte de acest tip. Rolul lor în ierarhii este

202

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

acela de clase de bază care grupează date şi metode comune. Din ele, prin specializare, se pot construi clase noi, care vor poseda caracteristicile clasei pe care o moştenesc şi, în plus, vor defini elemente (membri) specifice. Aşa de exemplu, clasa Contur poate da naştere unei clase Linie, de obiecte de tip linie dreaptă, iar clasa Suprafaţa conduce la clasele Dreptunghi şi Cerc, ca obiecte cu suprafaţă specifică. Clasele abstracte pot fi definite la diferite niveluri în arbore, fiind posibil ca o clasă abstractă să fie părinte pentru o altă clasă abstractă mai specializată. De regulă, o clasă abstractă este rădăcină în orice ierarhie. Deşi clasele abstracte nu se instanţiază, este permis să se declare referinţe către acestea. Acest lucru asigură polimorfismul şi, la limită, o referinţă de clasă rădăcină poate recepţiona adresa oricărui obiect din ierarhie şi, în consecinţă, poate asigura tratarea dinamică a obiectelor polimorfice. Uneori, clasele abstracte sunt utilizate pentru a grupa clase care nu au date comune, dar participă împreună la realizarea programelor. Aceste clase sunt atât de generale încât, de regulă, se reduc la un constructor vid şi, eventual, la un destructor vid şi virtual. Ele se justifică prin aceea că, în virtutea polimorfismului, permit aplicarea unui cod comun şi general la obiectele acestor clase, cum ar fi constituirea de liste eterogene. Definirea unei clase abstracte drept clasă de bază a unei ierarhii conduce la posibilitatea de a utiliza aceeaşi clasă pentru toate ierarhiile. Principiul este aplicat, în general, la construirea bibliotecilor de clase pe care limbajele orientate obiect le oferă. De exemplu, în Turbo Pascal, biblioteca denumită Turbo Vision are drept clasă primordială pentru toate ierarhiile clasa TObject. Astfel se “leagă” între ele diferitele ierarhii şi se poate utiliza facilitatea de polimorfism.

10.4.2 Definirea metodelor După conturarea claselor, din punctul de vedere al membrilor de tip dată, următoarea etapă a proiectării este definirea metodelor, ţinând cont de comportamentul dorit al diferitelor tipuri de obiecte. În cazul considerat, obiectele se creează, se afişează, se ascund, îşi schimbă culoarea de contur ori modul de umplere etc. (figura 10.11). În legătură cu definirea metodelor se fac următoarele precizări: • Fiecare clasă trebuie să posede un constructor. După modelul Turbo Vision, se recomandă a fi denumit INIT. Constructorul unei clase trebuie să apeleze constructorul clasei părinte, pentru a se asigura iniţializarea părţii moştenite. • Fiecare clasă care utilizează alocarea dinamică pentru spaţiu extins trebuie să posede un destructor, denumit DONE, declarat virtual. • Vor fi declarate virtuale, la nivelul claselor strămoş, toate metodele care urmează să fie redefinite în clasele derivate. Aceste metode trebuie să fie redeclarate ca virtuale în toţi descendenţii. • Dacă o clasă instanţiabilă conduce la obiecte vizuale (grafice, texte), atunci clasa trebuie să posede o metodă proprie de afişare (se spune că obiectele se

203

Obiecte în Pascal

autoafişează). Metoda de afişare se recomandă să fie definită ca virtuală, pentru a se asigura o legare dinamică adecvată. • Pentru a asigura satisfacerea deplină a principiului abstractizării şi încapsulării datelor, se recomandă utilizarea secţiunii PRIVATE pentru datele specifice clasei şi care nu intră direct în interfaţă. În acest sens trebuie prevăzute, pentru fiecare câmp, o metodă de consultare şi o metodă de modificare a valorii. • Deoarece programele realizate prin tehnica OOP sunt de talie mare, se recomandă utilizarea memoriei dinamice, care trebuie eliberată sistematic, eventual prin destructori adecvaţi.

10.4.3 Tratarea erorilor În realizarea metodelor unei clase apar, de regulă, două tipuri de erori: de domeniu şi de alocare. • Erorile de domeniu sunt generate de nerespectarea domeniului de definire a metodei (de exemplu, pentru o clasă stivă, operaţiile de citire şi ştergere nu sunt definite în cazul în care lista este vidă). Erorile nu sunt tratate de limbaj, fiind sarcina programatorului să prevadă mecanisme adecvate de comunicare între metodele claselor şi utilizatorul acestora. După “agentul” care provoacă situaţiile de eroare, se pot aplica, în general, două strategii: a) Dacă eroarea este determinată de starea obiectului curent, adică de obiectul cu care urmează să se apeleze o anumită metodă, atunci este detectabilă înainte de apel. Clasa prevede metode de detecţie, ca funcţii booleene, pe fiecare tip de eroare sau o funcţie unică pentru a sesiza prezenţa erorii. Obiectul curent va utiliza o funcţie de detecţie adecvată şi va proceda în consecinţă, adică va apela sau nu metoda respectivă. De exemplu, pentru un obiect de tip listă, o funcţie Empty poate testa dacă pointerul pentru capul listei are valoarea NIL, întorcând valoarea True, dacă lista este vidă sau False, în caz contrar.

204

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Figura x, y EsteVizibil Culoare INIT AFISEAZA ASCUNDE ARIE SCHIMBACULOARE Punct AFISEAZA ASCUNDE

Contur TipLinie GrosimeLinie INITDEFAULT SCHIMBACONTUR

Linie XSfarsit YSfarsit Init AFISEAZA ASCUNDE

Suprafata TipHasura CuloareUmplere INITDEFAULT SCHIMBAUMPLERE

Dreptunghi Latime Inaltime INIT AFISEAZA ASCUNDE ARIE

Cerc Raza INIT AFISEAZA ASCUNDE ARIE

Fig. 10.11 Ierarhie de obiecte grafice

b) Dacă situaţiile de eroare sunt datorate stării altor obiecte, care se primesc ca parametri în metoda aplicată, atunci strategia anterioară nu este aplicabilă, deoarece numai metoda în cauză este în măsură să sesizeze astfel de cazuri prin testarea parametrilor săi. În consecinţă, modul în care s-a terminat execuţia metodei se transmite ca o informaţie apelatorului. Ea trebuie tratată ca o informaţie globală metodei sau chiar clasei, deoarece este independentă de obiectul cu care s-a apelat metoda. Având în vedere încapsularea claselor în unităţi de program, astfel de informaţii pot fi valori ale unor variabile globale, declarate în secţiunea INTERFACE. Variabila globală poate fi de tip enumerativ, tip în care constantele definesc situaţiile de eroare. O declarare pentru aceste elemente poate avea, de exemplu, forma:

205

Obiecte în Pascal INTERFACE Type TipError=(OK, ZeroNum, TooBigNum); Var classErr:TipError;

La începutul metodelor implicate, variabilele de eroare se pun pe constanta care semnifică succes şi, după testările parametrilor, se modifică adecvat. Este sarcina apelatorului metodei ca, la reîntoarcerea controlului din metodă, să testeze dacă execuţia s-a realizat cu succes. • Erori de alocare. Alocarea spaţiului dinamic pentru obiecte sau a spaţiului extins al acestora poate eşua. O astfel de situaţie conduce la abandonarea programului. Programatorul trebuie să construiască un mecanism propriu de tratare a situaţiei. El se bazează pe utilizarea variabilei HeapError, procedura Fail şi posibilitatea de a iniţializa o unitate. Se procedează astfel: ◊ În fiecare constructor sau metodă care utilizează alocarea dinamică trebuie să se testeze valoarea referinţei returnată de funcţia sau procedura de alocare. Dacă a apărut o situaţie de eroare de alocare, procedura poate prevedea cod pentru a recupera eroarea, dacă acest lucru este posibil sau poate să încheie în ordine execuţia apelând procedura Fail. De exemplu: Constructor TA.Init; Begin .................................................... new (Ptr1); {alocare de spatiu dinamic} new(Ptr2); .................................................... if (Ptr1=Nil) or (Ptr2=Nil)……. Then begin {cod pentru recuperare} sau {cod de terminare + TA.DONE + Fail;} end; {continuare constructor} End;

◊ În partea de implementare a unităţii de program a clasei se defineşte o funcţie proprie, având un argument de tip WORD, care este utilizată în locul funcţiei standard ce se apelează în caz de lipsă de spaţiu: {$F+} {adresare far} Function fallocerr (a:Word): Integer; Begin fallocerr:=1 End;

◊ În partea de iniţializare a unităţii de program se introduce secvenţa:

206

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Begin HeapError:=@fallocerr; End.

În acest mod se înlocuieşte funcţia spre care punctează variabila HeapError, care întoarce valoarea zero în caz de lipsă de spaţiu, cu o funcţie proprie care returnează unu. Aceasta face ca sistemul să continue execuţia programului şi în cazul de eroare de spaţiu, făcând posibilă tratarea proprie a situaţiei.

207

ANEXA 1 UNITATEA DOS 1. Tipuri de date Tipul Registers Registers = Record CASE INTEGER OF 0: (AX,BX,CX,DX,BP,SI,DI,DS,ES,Flags:WORD); 1: (AL,AH,BL,BH,CL,CH,DL,DH:BYTE); End; Registrele AX, BX, CX şi DX sunt de uz general. Pe lângă referirea globală, ca registre de 16 biţi, pot fi referite şi semiregistre de opt biţi (un octet), la nivelul octetului inferior, prin AL, BL, CL, DL, respectiv al celui superior, prin AH, BH, CH, DH. Registrul BP (base pointer) conţine valoarea deplasării curente în segmentul de stivă. Registrele SI şi DI corespund unor deplasări ale operandului sursă şi destinaţie în cadrul segmentului de date, iar DS şi ES reprezintă adresele de segment ale acestor operanzi. Registrul Flags conţine indicatorii de control şi de stare (flags) ai procesorului, asociaţi unora dintre cei 16 biţi ai săi, poziţionaţi în funcţie de rezultatele executării fiecărei operaţii. Din perspectiva referirii prin programe Turbo Pascal, prezintă interes următorii indicatori de control (sau de condiţie): • CF (Carry Flag): este setat la transportul în exterior al celui mai semnificativ bit în operaţiile de calcul cu numerele reprezentate pe 16 biţi; • PF (Parity Flag): este setat dacă octetul inferior al rezultatului unei operaţii conţine un număr par de biţi cu valoarea unu. Este folosit cu prioritate de programele de comunicaţie; • AF (Auxiliary carry Flag): este setat dacă cei patru biţi de ordin inferior ai unei operaţii generează un transport în exterior (folosit pentru aritmetica BCD (Binary Coded Decimal); • ZF (Zero Flag): este setat dacă rezultatul unei operaţii este zero; • SF (Sign Flag): este egal cu bitul de ordin superior al rezultatului unei operaţii de calcul ce foloseşte operanzi cu semn (0 - pozitiv, 1 negativ); • OF (Overflow Flag): este setat dacă o operaţie de calcul cu operanzi cu semn produce un rezultat prea mare (sau prea mic, operaţie numită subdepăşire underflow) pentru a putea să fie reprezentat în zona rezultatului. Unitatea Dos conţine mai multe constante simbolice predefinite asociate acestor indicatori. Data şi ora DateTime = Record Year

: WORD;

{Anul}

208

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Month Day Hour Min Sec

: WORD; : WORD; : WORD; : WORD; : WORD;

{Luna} {Ziua} {Ora} {Minutul} {Secunda}

Fill Attr Time Size Name

: ARRAY[1..21] OF BYTE; : BYTE; : LONGINT; : LONGINT; : STRING[12];

End; Articol pentru căutare SearchRec = Record

End; Semnificaţia câmpurilor este următoarea: Fill - rezervat pentru sistemul de operare; câmpul nu trebuie modificat în program. Attr - atributele fişierului (valoarea octetului $0B din intrarea fişierului în [sub]director). Time - data şi ora ultimei scrieri în fişier, sub formă împachetată (valoarea, ca LONGINT, a octeţilor $16-$19 din intrarea fişierului în [sub]director). Size - lungimea în octeţi a fişierului (valoarea câmpului $1C-$20 din intrarea fişierului în [sub]director). Name - numele fişierului urmat de punct şi extensia lui (valoarea câmpurilor $0-$7 şi $8-$A din intrarea fişierului în [sub]director). Tipuri pentru caracteristicile fişierelor PathStr = STRING[79];⇒ Specificatorul complet; DirStr = STRING[67];⇒ Unitatea şi calea; NameStr = STRING[8]; ⇒ Numele propriu-zis; ExtStr = STRING[4]; ⇒ Extensia. Structura FIB pentru fişiere cu tip şi fără tip FileRec = Record Handle : WORD; Mode : WORD; RecSize : WORD; Private : ARRAY[1...16] OF BYTE; UserData : ARRAY[1...16] OF BYTE; Name : ARRAY[0...79] OF CHAR; End;

209

Unitatea DOS

Structura FIB pentru fişierele TEXT TextRec = RECORD Handle Mode BufSize Private BufPos BufEnd BufPtr OpenFunc InOutFunc FlushFunc UserData Name Buffer END;

: WORD; : WORD; : WORD; : WORD; : WORD; : WORD; : ^TextBuf; : Pointer; : Pointer; : Pointer; : ARRAY[1...16] OF BYTE; : ARRAY[0...79] OF CHAR; : TextBuf;

Semnificaţia principalelor câmpuri este următoarea: • Handle este o valoare întreagă care reprezintă identificatorul pentru fişiere deschise. Valorile 1-7 sunt utilizate pentru dispozitivele standard de I/E (intrare, ieşire, auxiliar, imprimanta, fişierele în curs de folosire cu comanda PRINT din DOS şi de reţea). Fişierele deschise ale utilizatorului au valori pentru handle începând cu 8. Fişierele nedeschise au Handle=0. O valoare handle asociată unui fişier devine disponibilă la închiderea acestuia. Într-un program pot fi deschise simultan maxim 12 fişiere ale utilizatorului (handle cu valori din intervalul [8, 19]). • Mode indică starea fişierului, care poate fi exprimată şi prin constantele publice specifice. Fişierele TEXT pot avea stările FmClosed, FmInput, FmOutput. Fişierele binare pot avea orice stare (implicit FmClosed sau FmInOut). • RecSize indică lungimea articolului (blocului) rezultată din descrierea internă programului. • Name este identificatorul extern al fişierului, aşa cum este precizat de procedura Assign. • BufSize reprezintă lungimea buffer-ului fişierului TEXT. • BufPos, BufEnd, BufPtr reprezintă pointeri folosiţi în gestiunea buffer-elor asociate fişierului TEXT. • OpenFunc, InOutFunc, CloseFunc, FlushFunc reprezintă adresa driver-elor pentru deschidere, citire/scriere, închidere a fişierului, golire a buffer-ului. • Buffer reprezintă zona tampon asociată fişierului (buffer-ul fişierului). Tipul TextBuf este definit în unit-ul Dos astfel: TextBuf = ARRAY[0...127] of CHAR. Pentru a avea acces la informaţiile din FIB, trebuie declarată o variabilă de tip FileRec (respectiv TextRec) care să aibă aceeaşi adresă cu FIB-ul (cu variabila de tip fişier). De exemplu: f: TEXT; inf_f: TextRec ABSOLUTE f.

210

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

2. Constante Atributele fişierelor (octetul $0B din intrarea unui fişier în [sub]director) ReadOnly = $01 ⇒ fişier numai pentru citire; Hidden = $02 ⇒ fişier ascuns; SysFile = $04 ⇒ fişier de sistem; Volumid = $08 ⇒ identificator de volum; Directory = $10 ⇒ [sub]director; Archive = $20 ⇒ fişier arhivă; AnyFile = $3F ⇒ orice fişier Constante asociate indicatorilor de stare Fcarry = $0001; Fparity = $0004; Fauxiliary = $0010; Fzero = $0040; FSign = $0080; Foverflow = $0800; Constantele corespund unor măşti cu care se poate testa dacă un anumit indicator este setat, valoarea lor fiind dată de poziţia indicatorului în cadrul registrului Flags. Astfel, indicatorul CF este plasat în bitul zero, deci masca asociată lui corespunde lui 20=$0001, indicatorul PF este plasat în bitul doi, cu masca 22=$0004 etc. Dacă se doreşte, de exemplu, să se verifice dacă ZF este setat, folosind o variabilă Reg de tipul Registers se va putea scrie: IF (Reg.Flags AND FZero) = 1 THEN ... Constante pentru starea fişierului FmClosed = $D7B0 FmInput = $D7B1 FmOutput = $D7B2 FmInOut = $D7B3

⇒ fişier închis; ⇒ fişier deschis pentru citire; ⇒ fişier deschis pentru scriere; ⇒ fişier deschis pentru citire/scriere;

3. Variabile DosError: INTEGER; Este iniţializată de funcţiile şi procedurile din acest unit, cu următoarele valori principale:0 - fără eroare; 2 - fişier negăsit; 3 - cale negăsită; 4 - prea multe fişiere deschise; 5 - acces interzis etc. Variabila DosError are valoarea zero când execuţia funcţiilor şi a procedurilor definite în unit-ul Dos se termină normal.

211

Unitatea DOS

4. Proceduri şi funcţii Prelucrarea fişierelor Procedure GetFAttr(VAR f; VAR attr:WORD); Returnează atributele unui fişier. Procedure SetFAttr(VAR f; VAR attr:WORD); Poziţionează (setează) atributele unui fişier. Procedure GetFTime(VAR f; VAR Dl:LONGINT); Returnează data şi ora ultimei scrieri într-un fişier. Dl este variabila în care se recepţionează data şi ora, sub formă "împachetată". Procedure SetFTime(VAR f; Dl:LONGINT); Înscrie data şi ora în eticheta fişierului. Dl este variabila ce conţine data şi ora sub formă împachetată, care se înscriu în intrarea fişierului în [sub]director. Procedure FindFirst(F_extern:PathStr; Attr:WORD; VAR zona:SearchRec); Caută într-un [sub]director prima intrare a unui fişier care are specificatorul şi atributul precizate în lista de parametri. F_extern este specificatorul extern al fişierului căutat. Specificatorul este format din cale, nume fişier şi, eventual, extensie. Când calea lipseşte se presupune [sub]directorul curent. Numele şi extensia fişierului pot fi generice (formate cu caracterele * sau ?). Attr reprezintă valoarea atributelor fişierului care se caută. Zona este o variabilă de tipul SearchRec, care va conţine informaţii despre fişierul f_extern în cazul când a fost găsit. Dacă f_extern nu este găsit, variabila zona rămâne nemodificată. Când f_extern este găsit, variabila DosError va avea valoarea zero. În caz contrar, DosError are valoare diferită de zero. Procedure FindNext(VAR zona:SearchRec); Caută într-un [sub]director următoarea intrare a unui fişier care are specificatorul şi atributul precizate la apelul anterior al procedurii FindFirst. Rezultă că procedura FindNext poate fi utilizată numai dacă, anterior, a fost apelată procedura FindFirst. Zona are aceeaşi semnificaţie ca la procedura FindFirst. Dacă următoarea intrare este găsită, variabila DosError va avea valoarea zero. În caz contrar, DosError are valoare diferită de zero. Function FSearch(Nume:PathStr; Lista:STRING):PathStr; Caută un fişier într-o listă de [sub]directori. Ea este asemănătoare comenzii PATH din MSDOS. Nume este variabilă de tip PathStr, care conţine numele fişierului de căutat. Lista conţine lista directoarelor în care se continuă căutarea, dacă fişierul nu a fost găsit în directorul curent. Căile specificate în listă trebuie separate prin caracterul ;. Funcţia returnează specificatorul extern al fişierului, în cazul în care îl găseşte. Când fişierul este în directorul curent, specificatorul extern furnizat este format din nume şi, eventual, extensie, iar când fişierul este în alt director, specificatorul este complet (cale+nume [+ extensie]). Când fişierul nu este găsit, funcţia returnează şirul vid. Funcţia nu verifică existenţa căilor. Când căile nu există, se returnează şirul vid.

212

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

Function FExpand(Nume:PathStr):PathStr; Expandează (completează) numele cu calea. Nume este variabilă de tip PathStr ce conţine numele care urmează să fie expandat cu calea. În procesul prelucrării fişierelor, funcţia are sens când este utilizată pentru extinderea cu componente (unitate, cale) furnizate implicit. Procedure FSplit(specificator:PathStr;VAR unit_dir:DirStr;VAR name:NumeStr;VAR ext:ExtStr); Descompune specificatorul extern (unitate, cale, nume, extensie) în trei componente: unitate+cale, nume, .extensie. Specificator este şirul care se analizează şi se descompune în componente. Unit_dir, nume şi ext sunt variabile în care se depun cele trei componente extrase din specificator. Când unitatea şi calea nu sunt prezente în specificator, se returnează valorile implicite. Când numele şi extensia nu sunt prezente în specificator, se returnează şiruri vide. Procedure SetVerify(v:BOOLEAN); Poziţionează comutatorul verify din DOS. Comutatorul are două valori: ON (valoare TRUE) sau OFF (valoare FALSE). Când comutatorul are valoarea ON, sistemul de operare face verificare după fiecare operaţie de scriere în fişiere (se verifică dacă datele scrise pot fi citite fără eroare). Poziţia ON a comutatorului verify măreşte timpul de execuţie a programelor. Când comutatorul are valoarea OFF, sistemul de operare nu face verificarea scrierii. Valoarea implicită a comutatorului este OFF. Valoarea comutatorului rămâne activă până la o nouă setare. V este o variabilă de tip BOOLEAN. Când v are valoarea TRUE, comutatorul verify primeşte valoarea ON; în caz contrar primeşte valoarea OFF. Procedura are efect similar cu comanda VERIFY din DOS. Dacă comutatorul verify este ON, rezultatul verificării scrierii este memorat în variabila DosError. Ocuparea discului Function DiskSize(Drive:BYTE):LONGINT; Returnează capacitatea, în octeţi, a discului montat în unitatea specificată. Drive specifică unitatea de disc, astfel: 0 - unitatea de disc curentă; 1 - unitatea de disc A; 2 - unitatea de disc B; 3 - unitatea de disc C etc. Funcţia returnează valoarea -1 dacă unitatea de disc nu există, dacă nu este montat disc în unitate sau dacă unitatea este defectă. Function DiskFree(Drive:BYTE):LONGINT; Returnează numărul de octeţi liberi de pe un disc montat într-o unitate. Drive are aceeaşi semnificaţie ca la funcţia DiskSize. Funcţia returnează valoarea -1 dacă unitatea de disc nu există, dacă nu este montat disc în unitate sau dacă unitatea este defectă.

213

Unitatea DOS

Execuţia programelor Procedure Exec(specif,parametru:STRING); Încărcarea în memorie şi lansarea în execuţie a programului aflat în fişierul specif. Parametrii de pe linia de comandă sunt specificaţi în parametru. Function DosExitCode:WORD; Codul de retur al unui proces (program). Întreruperi Procedure Intr(ni:BYTE; VAR reg:Registers); Lansarea întreruperii ni. În variabila reg se pregătesc argumentele. Procedure MsDos(VAR reg:Registers); Lansarea întreruperii $21. În variabila reg se pregătesc argumentele. Procedure GetIntVec(int:BYTE; VAR adrint:POINTER); Obţinerea, în adrint, a adresei întreruperii cu numărul int. Procedure SwapVectors; Salvarea vectorilor de întrerupere. Data şi timpul Procedure GetDate(VAR an, luna, ziua_luna, ziua_sapt:WORD); Obţinerea datei din sistem. Procedure SetDate(VAR an, luna, ziua:WORD); Poziţionarea datei în sistem. Procedure GetTime(VAR ora, minut, sec, sec100:WORD); Obţinerea timpului din sistem. Procedure SetTime(VAR ora, minut, sec, sec100:WORD); Poziţionarea timpului în sistem. Procedure PackTime(VAR Dc:DateTime; VAR Dl:LONGINT); "Împachetarea" unui articol de tip DateTime într-o dată de tip LONGINT. Dc este data şi ora, exprimate pe componente, care urmează să fie "împachetate" sub formă LONGINT în zona Dl. Procedure UnpackTime(Dl:LONGINT, VAR Dc:DateTime); "Despachetarea" unei valori de tip DateTime. Dl este expresie de tip LONGINT care va fi "despachetată" pe componentele datei şi orei în zona Dc. Alte funcţii Function DosVersion:WORD; Returnează un cuvânt ai cărui octeţi conţin numărul principal al versiunii curente a sistemului de operare (octetul inferior), respectiv numărul secundar (octetul superior).

214

ANEXA 2 UNITATEA CRT 1. Constante Constante pentru culorile de fond şi de text Culori de fond şi text Constantă simbolică Constantă (cod) Black (negru) 0 Blue (albastru) 1 Green (verde) 2 Cyan (turcoaz) 3 Red (roşu) 4 Magenta (violet) 5 Brown (maro) 6 LightGray (gri deschis) 7

Culori de text Constantă simbolică DarkGray LightBlue LightGreen LightCyan LightRed LightMagenta Yellow White

Constantă (cod) (gri închis) 8 (albastru deschis) 9 (verde deschis) 10 (turcoaz deschis) 11 (roşu deschis) 12 (violet deschis) 13 (galben) 14 (alb) 15

Blink=128; {Adunată la codul culorii se obţine afişare intermitentă} Constante care definesc modurile text Constantă simbolică BW40 BW80 Mono CO40 CO80 Font8x8

Constantă (cod) 0 2 7 1 3 256

Explicaţie Adaptor color negru, rezoluţie 40 x 25 Adaptor color negru, rezoluţie 80 x 25 Adaptor monocrom negru/alb, rezoluţie 80 x 25 Adaptor color, rezoluţie 40 x 25 Adaptor color,rezoluţie 80 x 25 Adaptor EGA/VGA, 43 şi 50 linii

2. Proceduri şi funcţii Pregătirea scrierii Procedure AssignCrt (VAR f:TEXT); Asignează fişierul f la dispozitivul CRT. Procedure TextMode (Mode:WORD); Selectează modul text. Procedure Window (X1, Y1, X2, Y2:BYTE); Defineşte fereastra text. Procedure TextBackground (Color:BYTE); Defineşte culoarea fondului. Procedure TextColor (Color:BYTE);

215

Unitatea CRT

Defineşte culoarea de scriere. Procedure HighVideo; Defineşte atributul de intensitate mare. Procedure LowVideo ; Defineşte atributul de intensitate mică. Procedure NormVideo; Restabileşte atributele implicite. Procedure ClrScr; Şterge fereastra curentă. Procedure ClrEol; Şterge caracterele liniei curente, de la poziţia cursorului până la sfârşit. Procedure DelLine; Şterge linia curentă; textul este defilat în sus cu o linie. Procedure InsLine; Inserează o linie goală în poziţia cursorului; textul este defilat în jos cu o linie. Procedure GotoXY (X, Y:BYTE); Mută cursorul pe linia Y, coloana X. Function WhereX: BYTE; Returnează numărul coloanei în care se găseşte cursorul. Function WhereY: BYTE; Returnează numărul liniei pe care se găseşte cursorul. Programarea difuzorul intern Procedure Sound (Hz:WORD); Porneşte difuzorul intern, care emite un sunet continuu de frecvenţă Hz. Procedure Delay (Ms:WORD); Introduce o întârziere în execuţia următoarei instrucţiuni. Procedure NoSound; Opreşte difuzorul intern. Lucrul cu tastatura Function KeyPressed: BOOLEAN; Întoarce TRUE dacă a fost apăsată o tastă. Function ReadKey: CHAR; Returnează codul caracterului rezultat prin apăsarea unei taste.

216

ANEXA 3 ERORI DE EXECUŢIE Apariţia unei erori de execuţie determină întreruperea programului şi afişarea unui mesaj de eroare, de forma: Run-time error nnn at xxxx:yyyy, unde nnn este codul erorii de execuţie, iar xxxx:yyyy este adresa ei (segment şi offset). Erorile de execuţie se împart în: erori DOS (coduri 1-99); erori de intrare/ieşire (coduri 100149), erori critice (coduri 150-199) şi erori fatale (coduri 200-255).

1. Erori DOS 1. Funcţie inexistentă. Generată de un apel al unei funcţii DOS inexistente. 2. Fişier inexistent. Generată de execuţia uneia din procedurile Reset, Append, Rename sau Erase, dacă identificatorul asignat variabilei de tip fişier nu corespunde unui fişier existent. 3. Cale inexistentă. Generată de execuţia uneia din procedurile: • Reset, Append, Rewrite, Rename sau Erase, dacă identificatorul asignat variabilei de tip fişier este invalid sau include un sub[director] inexistent; • ChDir, MkDir sau RmDir, dacă sub[directorul] este invalid sau inexistent. 4. Prea multe fişiere deschise. Generată de execuţia uneia din procedurile Reset sau Append dacă, la un moment dat, sunt deschise simultan mai mult de 12 fişiere ale utilizatorului. Dacă se doreşte raportarea erorii pentru un număr mai mic de fişiere deschise simultan, trebuie ca fişierul CONFIG.SYS să nu conţină clauza FILES=xx sau să specifice numărul de fişiere dorit. 5. Acces interzis la fişier. Generată de execuţia uneia din procedurile: • Reset sau Append, dacă FileMode permite scrierea, dar identificatorul asignat variabilei fişier specifică un [sub]director/fişier read-only; • Rewrite, dacă sub[directorul] este plin sau identificatorul asignat variabilei fişier specifică un [sub]director/fişier existent read-only; • Rename, dacă identificatorul asignat variabilei fişier specifică un fişier existent; • Erase, dacă identificatorul asignat variabilei fişier specifică un sub[director]/fişier read-only; • MkDir, dacă: există un fişier cu aceleaşi nume în sub[directoru] părinte; nu există spaţiu în sub[directorul] părinte; este specificat în cale un dispozitiv; • RmDir, dacă: sub[directorul] nu este vid; nu se specifică un sub[director] în cale; directorul specificat include rădăcina; • Read/BlockRead pentru un fişier cu tip/fără tip, dacă acesta nu a fost deschis pentru citire;

217

Erori de execuţie

• Write/BlockWrite pentru un fişier cu tip/fără tip, dacă acesta nu a fost deschis pentru scriere. 6. Handle de fişier invalid. Generată la transmiterea unui handle (vezi §8.2) invalid de fişier, la un apel al sistemului DOS. 12. Cod invalid de acces la fişier. Generată de execuţia uneia din procedurile Reset sau Append pentru fişiere cu tip/fără tip, dacă valoarea variabilei FileMode este invalidă. 15. Număr dispozitiv invalid. Generată de execuţia uneia din procedurile GetDir sau ChDir, dacă numărul dispozitivului periferic este invalid. 16. Sub[directorul] curent nu poate fi suprimat. Generată de execuţia procedurii RmDir, dacă în calea specificată este inclus directorul curent. 17. Redenumire fişiere pe dispozitive diferite. Generată de execuţia procedurii Rename, dacă specificatorii de fişiere nu sunt pe acelaşi dispozitiv.

2. Erori de intrare/ieşire Erorile de intrare/ieşire determină întreruperea execuţiei programului, numai dacă instrucţiunea respectivă a fost compilată cu directiva {$I+} (valoare impicită). În cazul în care se specifică directiva de compilare {$I-}, execuţia programului continuă, iar apariţia erorii este depistată cu ajutorul funcţiei IOResult. 100 Eroare la citirea de pe disc. Generată de execuţia procedurii Read pentru fişiere cu tip, dacă se încearcă citirea sfârşitului de fişier. 101 Eroare la scrierea pe disc. Generată de execuţia uneia din procedurile Close, Write, WriteLn, Flush sau Page, dacă s-a umplut discul (nu mai este spaţiu pe disc). 102 Fişier neasignat. Generată de execuţia uneia din procedurile Reset, Rewrite, Append, Rename sau Erase, dacă variabila fişier nu a fost asignată unui nume fizic, prin procedura Assign. 103 Fişier nedeschis. Generată de execuţia uneia din procedurile/funcţiile Close, Read, Write, Seek, Eof, FilePos, FileSize, Flush, BlockRead sau BlockWrite, dacă fişierul nu este deschis. 104 Fişier nedeschis pentru intrare. Generată de execuţia uneia din procedurile/funcţiile Read, ReadLn, Eof, EoLn, SeeKEof sau SeeKEoln, dacă fişierul TEXT respectiv nu este deschis pentru consultare. 105 Fişier nedeschis pentru ieşire. Generată de execuţia uneia din procedurile Write sau WriteLn, dacă fişierul TEXT respectiv nu este deschis pentru creare/extindere. 106 Format numeric invalid. Generată de execuţia uneia din procedurile Read sau ReadLn, dacă o valoare numerică citită dintr-un fişier TEXT nu concordă cu formatul numeric declarat.

218

Programarea calculatoarelor – Tehnica programării în limbajul Pascal

3. Erori critice 150 151 152 153 154 155 156 157 158 159 160 161 162

Disc protejat la scriere Unit necunoscut Dispozitivul nu este pregătit Comandă necunoscută Eroare CRC în dată Cerere pe un dispozitiv greşit Eroare de poziţionare pe disc Tip dispozitiv necunoscut Sector negăsit Imprimantă în aşteptarea hârtiei Incident la scrierea pe dispozitiv Incident la citirea de pe dispozitiv Întrerupere hardware

4. Erori fatale 200 Împărţire la zero. Generată de împărţirea la 0 a unui număr, cu operatorii / , MOD sau DIV. 201 Nonapartenenţă la un interval. Generată de instrucţiunile compilate cu directiva {$R+}, în următoarele condiţii: • expresia de indice pentru referirea unui element de masiv este în afara intervalului; • atribuirea unei valori în afara intervalului stabilit pentru variabila respectivă; • atribuirea unei valori în afara intervalului stabilit pentru un parametru de procedură/funcţie. 202 Depăşire stivă. Generată la apelul unei proceduri/funcţii, compilate cu directiva {$S+}, când nu este spaţiu suficient în stivă pentru memorarea variabilelor locale. Stiva se poate mări cu directiva de compilare {$M}. Eroarea apare şi în cazul unui apel recursiv infinit. 203 Depăşire heap. Generată de execuţia uneia din procedurile New sau GetMem, când nu este suficient spaţiu în heap, pentru alocarea unui bloc sau a unei zone de mărime specificată. 204 Operaţie cu pointer invalid. Generată de execuţia uneia din procedurile Dispose sau FreeMem dacă: pointerul are valoarea nil sau indică o locaţie în afara zonei heap; lista blocurilor libere nu poate fi extinsă, deoarece este plină; HeapPtr are o valoare prea apropiată de limita inferioară a listei libere. 205 Depăşire virgulă mobilă. Generată în urma unei operaţii al cărei rezultat este un număr prea mare pentru a fi reprezentat într-un tip real de dată Pascal. 206 Depăşire inferioară virgulă mobilă. Generată în urma unei operaţii al cărei rezultat este un număr prea mic pentru a fi reprezentat într-un tip real de dată Pascal.

219

Erori de execuţie

Apare numai dacă se utilizează coprocesorul matematic 8087. Se transmite, implicit, valoarea zero. 207 Operaţie virgulă mobilă invalidă. Generată dacă: • Argumentul funcţiilor Trunc sau Round este în afara intervalului [-2147483648, 2147483647]; • Argumentul funcţiei Sqrt este negativ; • Argumentul funcţiei Ln este negativ sau zero; • A apărut o depăşire a stivei 8087. 208 Managerul de reacoperire nu este instalat. Generată în urma apelului unei funcţii/proceduri de reacoperire, în cazul în care componenta de gestiune a structurilor de reacoperire (Overlay Manager) nu a fost instalată (cel mai adesea nu s-a apelat procedura OvrInit sau apelul ei a eşuat). 209 Eroare la citirea unui fişier de reacoperire. Generată în cazul în care se produce o eroare când managerul de reacoperire încearcă să citească un unit dintr-un fişier de reacoperire.

220

BIBLIOGRAFIE 1.

Apostol, C., Roşca, I., Gh., Ghilic-Micu, B., Roşca, V.

Introducere în programare. Teorie şi practică Pascal, Casa de editură şi presă Viaţa Românească, Bucureşti, 1993

2.

Apostol, C., Roşca, I., Gh., Ghilic-Micu, B., Roşca, V.

Prelucrarea fişierelor în Pascal, Editura Tehnică, Bucureşti, 1994

3.

Ionescu, A., Cocianu, C.

Fundamentele programării. Probleme rezolvate şi propuse, Editura ASE, Bucureşti, 2000

4.

Findlay, W., Watt, D.

Pascal. An Introduction to Methodical Programming, Pitman Publishing, 1987

5.

Knuth, D.

The Art of Computer Programming, Vol. 2, Seminumerical Algorithms, Addison-Wesley, 1981

6.

Knuth, D.

The Art of Computer Programming, Vol. 2, Seminumerical Algorithms, Addison-Wesley, 1981

7.

Roşca, I., Gh., Apostol, C., Ghilic-Micu, B., Roşca, V.

Programare sistematică în Pascal, Editura Didactică şi Pedagogică, Bucureşti, 1998

8.

Roşca, I., Gh., Apostol, C., Ghilic-Micu, B., Stoica, M., Cocianu, C., Uscatu, C.

Bazele elaborării programelor. Exerciţii rezolvate şi propuse, Editura ASE, Bucureşti, 1999

9.

Stoer, I., Bulirsh, R.

Introduction to Numerical Analysis, Springer-Verlag, 1980

221

Related Documents