Sunteți pe pagina 1din 212

Cuprins

1. Tipuri dinamice de date. Pointeri ...................................................................... 3

2. Subprograme .......................................................................................................... 13

3. Subprograme recursive ....................................................................................... 30

4. Tipul de dată ARTICOL ...................................................................................... 44

5. Fişiere de date ...................................................................................................... 52

6. Algoritmi de prelucrare a fişierelor binare ................................................... 72

7. Structuri dinamice de date. Liste .................................................................... 110

8. Grafuri .................................................................................................................... 122

9. Structuri arborescente ...................................................................................... 160

10. Elemente de programare orientată obiect ................................................... 186

Bibliografie ................................................................................................................. 212


1 Tipuri dinamice de date. Pointeri

Pointerul este un tip de dată predefinit, care are ca valoare adresa unei
zone de memorie (figura 1.1).
Memoria internă

Segment:offset

Zona de memorie indicată de pointer


Pointer
Figura 1.1 Un pointer este adresa unei alte zone de memorie
Folosirea pointerilor prezintă următoarele avantaje:
• înlocuirea expresiilor cu indici – înmulţirile din formula de calcul al
rangului se transformă în adunări şi deplasări;
• posibilitatea alocării dinamice a memoriei;
• folosirea tipurilor procedurale de date;
• calculul adreselor.

În operaţiile cu pointeri se folosesc următorii operatori specifici:


Operatori Simbol Utilizare
Operator de referenţiere * tip*
Operator de referenţiere & &nume
Operator de dereferenţiere * *nume
* ⇒ defineşte un nou tip de dată (pointer la tip);
& ⇒ extrage adresa unei variabile (creează o referinţă);
* ⇒ accesează conţinutul zonei de memorie indicate de pointer.

Cei doi operatori au efect invers: *&nume Ù nume.


Exemplu: *&nume reprezintă valoarea de la adresa variabilei nume (valoarea
variabilei nume).

3
Tipuri dinamice de date. Pointeri

1.1 Declararea şi iniţializarea pointerilor


Fie TIP un tip de dată oarecare în limbajul C (inclusiv void). Declararea
TIP* nume; este o declaraţie de pointer. TIP* este un nou tip de dată denumit
pointer spre TIP, iar nume este o variabilă de tipul pointer spre TIP.

Exemple:
• int* n; ⇒ n este o variabilă de tip pointer spre întreg;
• struct complex {a,b:real;}* x; ⇒ x este o variabilă de tip pointer spre o
structură de tipul complex;
• void* p; ⇒ p este o variabilă de tip pointer spre void; p poate primi ca
valoare adresa unei zone de memorie de orice tip.

Dacă TIP este un tip oarecare (mai puţin void) atunci tipul TIP* este adresa
unei zone de memorie de un tip cunoscut. Operaţiile care se pot efectua asupra
zonei respective de memorie sunt definite de tipul acesteia. Dacă TIP este void,
atunci TIP* este adresa unei zone de memorie de tip necunoscut. Deoarece nu se
cunoaşte tipul zonei de memorie, nu sunt definite operaţiile care se pot efectua
asupra ei.
Pentru pointerii din exemplele anterioare se rezervă în memoria principală
(în segmentul de date) câte o zonă de 4B în care se va memora o adresă (sub forma
segment:offset).
Când variabila nume nu este iniţializată prin declarare, ea primeşte implicit
valoarea NULL. La execuţie, poate primi ca valoare adresa unei variabile numai de
tipul TIP. Dacă TIP este void, atunci nume poate primi adresa oricărei variabile, de
orice tip.

Exemple:
int* nume; int a; float b;
nume = &a; => este o atribuire corectă; nume are ca valoare adresa variabilei a.
nume = &b; => este o atribuire incorectă; nume poate primi ca valoare doar
adresa unei variabile întregi.
void* nume;
int a; float b;
nume = &a;
nume = &b; => ambele atribuiri sunt corecte; nume poate primi ca valoare
adresa oricărei variabile, de orice tip.
Iniţializarea pointerilor se poate realiza ca în exemplul precedent sau, ca şi
pentru celelalte variabile, la declarare, astfel:

int a; int* nume=&a;

Se observă folosirea operatorului de referenţiere & pentru a crea o referinţă


către variabila a. La alocarea dinamică a memoriei se foloseşte o altă metodă
pentru iniţializarea unui pointer. Operatorul de dereferenţiere se utilizează atât

4
Programarea calculatoarelor

pentru definirea tipului pointer, cât şi pentru referirea datelor de la adresa indicată
de pointer.

Exemplu:
int a,b,c; int* nume;
void* nume2;
b=5;
nume=&a;
*nume=b;
c=*nume+b;
nume2=&b;
*(int*)nume2=10;
c=*(int*)nume2;

Se observă folosirea conversiei de tip (typecasting), atunci când se lucrează cu


pointeri spre tipul void (fără tip). Chiar dacă un pointer spre tipul void poate primi
ca valoare adresa unei variabile de orice tip, pentru a putea lucra cu ea este
necesară gestionarea corectă a tipului operanzilor.

1.2 Utilizarea pointerilor


1.2.1 Operaţii cu pointeri

Asupra pointerilor se pot efectua operaţii aritmetice. Fie secvenţa:


int *nume,*nume2, c, a, b; nume=&a; nume2=&a;

Incrementare/decrementare
Dacă nume este pointer spre un tip TIP, prin incrementare/decrementare,
valoarea lui nume se incrementează/decrementează cu numărul de octeţi necesari
pentru a memora o dată de tip TIP, adică cu sizeof(TIP).

nume++ Ù nume are ca valoare o adresă care este incrementată şi primeşte


valoarea nume+sizeof(int) (care este adresa lui b);
nume2-- Ù nume are ca valoare o adresă care este decrementată şi primeşte
valoarea nume-sizeof(int) (care este adresa lui c);

Situaţia iniţială este următoarea:

nume nume2 c a b
4B 4B 2B 2B 2B

5
Tipuri dinamice de date. Pointeri

După cele două operaţii:

nume nume2 c a b
4B 4B 2B 2B 2B
Analog se execută operaţiile ++nume şi --nume.

Exemplu:
float v[20];
float* p;
int i;
p=&v[i]; => i poate avea valori între 0 şi 19

În urma atribuirii ++p sau p++, p va avea ca valoare adresa lui v[i] plus 4 octeţi,
adică adresa lui v[i+1].

Adunarea/scăderea unui întreg


În general, dacă p este un pointer spre un tip TIP, atunci când se adună un
întreg n la pointerul p, rezultatul va fi tot un pointer spre TIP, care are ca valoare
adresa memorată în p, la care se adună de n ori numărul de octeţi necesari pentru a
memora o dată de tip TIP, adică n*sizeof(TIP). Asemănător se execută scăderea
unui întreg dintr-un pointer.
nume+n Ù nume primeşte valoarea nume+n*sizeof(int)
nume-n Ù nume primeşte valoarea nume-n*sizeof(int)

Exemplu: Fie p şi q pointeri spre tipul float (float* p, *q). Presupunând că p a


fost iniţializat cu valoarea 0x0fff:0x3450, în urma operaţiei q=p+3, q primeşte
valoarea 0xfff:0x345c (se adună 3*4 octeţi). În urma operaţiei q=p-2, q primeşte
valoarea 0xffff:0x344a (se scad 2*4 octeţi).

Operaţiile descrise anterior se folosesc frecvent în lucrul cu masive.

Compararea a doi pointeri


Limbajul C permite compararea a doi pointeri într-o expresie, folosind
oricare din operatorii relaţionali (==, !=, <, >, <=, >=).
Rezultatul expresiei nume op nume2 (unde op este unul din operatorii
precizaţi anterior) este adevărat (nenul) sau fals (zero) după cum nume este egal,
mai mare sau mai mic decât nume2. Doi pointeri sunt egali dacă adresele care
constituie valorile lor sunt egale. Privind memoria internă liniar, începând de la
0x0000:0x0000, un pointer p este mai mare decât altul q, dacă adresa pe care o
conţine p este mai îndepărtată de începutul memoriei decât adresa conţinută de q.
Este permisă şi compararea unui pointer cu o valoare constantă. Uzual se
foloseşte comparaţia cu valoarea NULL pentru a verifica dacă pointerul a fost
iniţializat (un pointer neiniţializat are valoarea NULL), folosind unul din operatorii
== sau !=. Valoarea NULL este definită în stdio.h astfel: #define NULL 0

6
Programarea calculatoarelor

De multe ori se preferă comparaţia directă cu zero (nume==0 sau


nume!=0). În loc de nume=0 se poate folosi expresia nume. Aceasta se
interpretează astfel: dacă nume nu a fost iniţializat, atunci are valoarea NULL
(adică 0), deci expresia este falsă. În caz contrar valoarea expresiei este nenulă,
deci adevărată. Asemănător se foloseşte expresia !nume.
Exemplu:
float* p,q,r,t;
float a,b;
p=&a; q=&b; r=&a;
a=5; b=7;
if(t) printf("Pointer initializat!\n");
else printf("Pointer neinitializat!\n");
if(p==r) printf("Pointeri egali\n");
else printf("Pointeri diferiti\n");
if(p>q) printf("%d\n",a);
else printf("%d\n",b);

Pe ecran se va afişa:
Pointer neinitializat!
Pointeri egali
7

deoarece t are valoarea NULL, variabilele p şi r au ca valoare adresa lui a, iar q


conţine adresa lui b, care este mai mare decât a lui a (datorită faptului că a a fost
alocat primul).

Diferenţa dintre doi pointeri


Fie secvenţa:
int m[50],* a, * b;
a=&m[i]; b=&m[j];

unde i şi j sunt întregi în intervalul [0..49]. Expresia a-b are valoarea i-j,
interpretată ca distanţă între adresele a şi b, exprimată în zone de memorie de
lungime sizeof(int).
Valoarea unei expresii diferenţă se calculează astfel: se face diferenţa între
cele două adrese (în octeţi), apoi se împarte la dimensiunea tipului de dată referită
de cei doi pointeri (tipul int în exemplul de mai sus – vezi figura 1.2). Cei doi
pointeri trebuie să refere acelaşi tip de dată, altfel rezultatul nu are semnificaţie.
Operaţia este utilă în lucrul cu masive.
m i j

a b

Figura 1.2 Reprezentarea semnificaţiei variabilelor din exemplul anterior

7
Tipuri dinamice de date. Pointeri

1.2.2 Legătura între pointeri şi masive

În limbajul C numele unui masiv este un pointer către tipul de dată al


elementele masivului.
Pentru masivele unidimensionale:
int m[50]; Ù m are tipul int*
int* p; Ù p are tipul int*
Diferenţa constă în faptul că zona de memorie către care punctează m este
rezervată la compilare (ceea ce nu se întâmplă în cazul pointerilor declaraţi ca
atare). De aceea m nici nu poate primi valori în timpul execuţiei programului (nu se
poate schimba adresa memorată în m). El memorează adresa primului element din
masiv. Referirea unui element m[i] este echivalentă cu *(m+i) – conţinutul de la
adresa m+i. Limbajul C nu face niciun fel de verificări în privinţa depăşirii
limitelor indicilor masivului, de aceea expresiile m[500] sau m[-7] vor fi
considerate corecte de compilator, existând riscul unor erori logice. Este sarcina
programatorului să se asigure că indicii nu vor depăşi limitele.

Pentru masivele bidimensionale:


int m[50][50]; Ù m are semnificaţia următoare: m[i][j] Ù
*(*(m+i)+j), reprezintă conţinutul de la adresa j plus conţinutul de la adresa
memorată în i plus m. Aceasta poate fi interpretată astfel: m este un pointer spre un
vector de pointeri, fiecare element al vectorului fiind la rândul lui un pointer spre o
linie a matricei (un vector de elemente de tip float). În acest fel se alocă matricele
în mod dinamic (figura 1.3).
Analog pot fi interpretate masivele cu mai multe dimensiuni.

m
m[0,0] m[0,1] … m[0,49]
m[0,0] m[0,1] … m[0,49]
m[0]
m[1] m[2,0] m[2,1] … m[2,49]
m[2]
m[3,0] m[3,1] … m[3,49]
m[3]
m[4] m[4,0] m[4,1] … m[4,49]

… … … …
m[49]
m[49,0] m[49,1] … m[49,49

Figura 1.3 Reprezentarea modului de alocare dinamică a spaţiului necesar


pentru memorarea unei matrice 50x50

8
Programarea calculatoarelor

Exemple:
• un masiv cu trei dimensiuni float m[10][10][10] poate fi interpretat ca un
pointer spre un vector de pointeri spre matrice;
• un masiv cu n dimensiuni este tratat ca un pointer spre un vector de pointeri
către masive cu n-1 dimensiuni.
Pentru a lucra cu elementele unei matrice se poate folosi adresarea
indexată (m[i] pentru vectori sau m[i][j] pentru matrice) sau adresarea elementelor
prin pointeri (*(m+i) pentru vectori sau *(*(m+i)+j) pentru matrice etc.). De
asemenea se poate declara un pointer iniţializat cu adresa de început a masivului,
iar elementele masivului să fie referite prin intermediul acestui pointer.
Exemple: float* v[10]; float* p;
p=v;
După atribuire, pointerul p conţine adresa de început a masivului şi poate fi folosit
pentru referirea elementelor masivului. De exemplu, v[3] şi p[3] referă aceeaşi
zonă de memorie.
Să se scrie secvenţa de program care citeşte de la tastatură elementele unei matrice,
folosind un pointer pentru adresarea elementelor matricei.
int m,n;
float a[10][10];
printf("Nr. linii:\n"; scanf("%d", &m);
printf("Nr. coloane:\n"); scanf("%d", &n);
for(i=0;i<m;i++)
for(j=0;j<n;j++)
{ printf("a(%d,%d)= ",i,j);
scanf("%f", *(m+i)+j );
}
Observaţie:
*(m+i)+j este un pointer, care conţine adresa elementului a[i][j]; în funcţia
scanf trebuie transmise ca parametri adresele unde se depun valorile citite; în
exemplul anterior se putea scrie &*(*(m+i)+j), şi, reducînd, rezultă *(m+i)+j.

1.2.3 Alocarea dinamică a memoriei

Pentru a memora o valoare de un anumit tip în heap este necesar să se


declare un pointer către acel tip de dată, apoi să se rezerve memoria necesară.
Pentru a rezerva spaţiu în heap se foloseşte funcţia standard:
void* malloc(unsigned n);

Funcţia rezervă o zonă de n octeţi în heap şi returnează adresa acesteia.


Deoarece funcţia returnează pointer spre void este necesară conversia spre tipul
dorit, astfel:
int* nume;
nume=(int *) malloc(sizeof(int)); ⇔ rezervă în heap spaţiu
pentru o valoare de tip întreg.

9
Tipuri dinamice de date. Pointeri

Eliberarea unei zone de memorie rezervate anterior se face prin funcţia


standard:

void free(void* p);

Funcţia primeşte ca parametru un pointer (indiferent de tip) spre zona de


memorie pe care trebuie să o elibereze.
Limbajul C oferă posibilitatea de a aloca contiguu zone de memorie pentru
mai multe date de acelaşi tip, prin funcţia standard:

void* calloc(unsigned nr_elem, unsigned dim_elem);

Funcţia calloc rezervă o zonă contiguă de memorie pentru mai multe


elemente de acelaşi tip, întorcând un pointer spre zona respectivă.

int* masiv;
masiv=(int*)calloc(50,sizeof(int)); ⇔ rezervă spaţiu de
memorie pentru un vector cu 50 de elemente întregi.

Există şi o variantă a lui malloc care returnează în mod explicit un pointer


„îndepărtat” (far):

void* farmalloc(unsigned long n);

Pentru eliberarea unei zone de memorie rezervate prin farmalloc se


foloseşte funcţia standard:
void farfree(void* p);

Exemple:
1. Alocarea de spaţiu în heap pentru o matrice.
int** m;
int n,p;
/* se alocă spaţiu pentru vectorul cu adresele celor n linii ale
matricei */
m=(int**)malloc(m*sizeof(int*));
for(int i=0;i<m;i++)
/*se alocă spaţiu pentru fiecare linie a matricei, cîte p elemente*/
m[i]=(int*)malloc(n*sizeof(int));

2. Să se scrie un subprogram pentru citirea de la tastatură a dimensiunii şi


elementelor unui vector memorat în heap.

void cit_vect(int *n, float **v)


{ int i;
printf("Nr. elemente: ");
scanf("%d ", n);
*v=(float*)malloc(*n*sizeof(float));

10
Programarea calculatoarelor

for(i=0;i<*n;i++)
{ printf("v(%d)= ",i);
scanf("%f",&(*v)[i]);
}
}

3. Să se scrie o funcţie care să citească cel mult n numere întregi şi le păstreze în


zona de memorie a cărei adresă de început este dată printr-un pointer. Funcţia
returnează numărul valorilor citite.

int cit_nr(int n, int* p)


{ int nr, i;
int* q=p+n; // q este adresa unde se termina zona
//rezervata pentru cele n numere
i=0;
while(p<q) //cit timp nu s-au citit n numere
{ printf("Numarul %d= ", i);
if(scanf("%d", &nr)!=1) break; //in caz de eroare la citire
//se termina ciclul
*p=nr;
p++;
i++;
}
return(i);
}

1.2.4 Modificatorul const

În limbajul C constantele simbolice se declară prin directiva de


preprocesare #define. O altă posibilitate de lucru cu constante este iniţializarea unei
variabile cu o valoare şi interzicerea modificării valorii acesteia. În acest scop se
foloseşte modificatorul const. Sunt permise următoarele forme de utilizare:

a) tip const nume = valoare; sau


const tip nume = valoare;

Declaraţia este echivalentă cu tip nume=valoare dar, în plus, nu permite


modificarea valorii lui nume printr-o expresie de atribuire nume = valoare_noua;
Faţă de o constantă simbolică, în acest caz se rezervă spaţiu de memorie în care se
înscrie valoarea constantei (constantă obiect).

b) tip const* nume = valoare; sau


const tip* nume = valoare;

Prin această declarare se defineşte un pointer spre o zonă cu valoare constantă. Nu


este permisă atribuirea de genul *nume=valoare_noua, dar se poate ca variabilei
nume să i se atribuie o adresă (de exemplu, nume = p, unde p este un pointer spre
tip). Pentru a modifica valoarea înscrisă în memorie la adresa memorată de

11
Tipuri dinamice de date. Pointeri

pointerul nume se poate folosi totuşi un alt pointer:


tip *t;
t=nume;
*t=valoare_noua;

c) const tip* nume;

Construcţia se foloseşte la declararea parametrilor formali, pentru a împiedica


modificarea lor în corpul subprogramelor, în cazul în care apelatorul are nevoie de
valorile iniţiale.

1.2.5 Tratarea parametrilor din linia de comandă

În linia de comandă a unui program pot să apară parametri (sau


argumente). Aceştia sunt şiruri de caractere despărţite prin spaţii. Programul poate
accesa argumentele prin intermediul parametrilor predefiniţi ai funcţiei main:

void main(int argc, char* argv[])

unde argc conţine numărul de parametri ai programului, incrementat cu 1.

Exemplu: Dacă programul nu are niciun parametru, argc are valoarea 1, dacă
programul are doi parametri, argc are valoarea 3 etc.
Variabila argv este un vector de pointeri care conţine adresele de memorie
unde s-au stocat şirurile de caractere care constituie parametrii programului. Primul
şir (argv[0]) conţine identificatorul fişierului (inclusiv calea completă) care
memorează programul executabil. Următoarele şiruri conţin parametrii în ordinea
în care au apărut în linia de comandă (parametrii în linia de comandă sunt şiruri de
caractere separate prin spaţii). Interpretarea acestor parametri cade în sarcina
programului.

Exemplu: Să se scrie un program care afişează parametrii din linia de comandă.

#include<stdio.h>
main(int argc, char *argv[]);
{ int i;
printf("Fisierul executabil: %s\n", argv[0]);
for(i=1;i<argc;i++)
printf("Parametrul nr. %d: %s\n",i, argv[i]);
}

12
2 Subprograme

Conform teoriei programării, subprogramele sunt clasificate în funcţii,


care returnează un singur rezultat prin „numele” funcţiei şi oricâte prin parametri
de ieşire şi proceduri, care returnează oricâte rezultate, prin intermediul
parametrilor de ieşire.
Un program C este un ansamblu de funcţii care realizează activităţi bine
definite. Există o funcţie, numită main(), care este apelată la lansarea în execuţie a
programului. Subprogramele C sunt, în mod nativ, funcţii. Pot fi construite
subprograme care nu returnează niciun rezultat prin numele lor, comportându-se ca
o procedură (conform definiţiei din teorie).
Sistemele C au colecţii de biblioteci care conţin funcţii standard. Textul
sursă al unui program C poate fi partiţionat în mai multe fişiere. Fiecare fişier
constă dintr-un set de funcţii şi declaraţii globale. Fişierele care constituie partiţia
pot fi compilate şi, eventual, testate separat, dar numai unul va conţine funcţia
main().

2.1 Construirea şi apelul subprogramelor


Funcţiile C sunt formate din antet şi un corp. Antetul are forma:

tip nume([lista-parametri-formali])
unde:
• tip poate fi un tip simplu de dată. Dacă lipseşte, este considerat tipul implicit
(int pentru unele compilatoare, void pentru altele);
• nume este un identificator care reprezintă numele funcţiei;
• lista-parametrilor-formali conţine parametrii formali sub forma:

[tip1 identificator1[,tip2 identificator[,tip3 identificator …]]]

13
Subprograme

Parametrii sunt separaţi prin virgulă. La limită, lista poate fi vidă. Pentru fiecare
parametru trebuie specificat tipul, chiar dacă mai mulţi parametri sunt de acelaşi
tip (nu este posibilă definirea de liste de parametri cu acelaşi tip).
Pentru funcţiile care nu întorc o valoare prin numele lor, tipul funcţiei va fi
void sau va fi omis.

Corpul este o instrucţiune compusă: conţine declaraţiile locale şi


instrucţiunile executabile care implementează algoritmul. Corpul funcţiei se
execută până la ultima instrucţiune sau până la executarea instrucţiunii return.
Forma ei generală este:

return(expresie); sau
return expresie; sau
return;

Prima şi a doua formă sunt folosite în cazul funcţiilor care returnează o


valoarea prin numele lor. Prin executarea acestei instrucţiuni se evaluează expresia,
valoarea sa este atribuită funcţiei şi se încheie execuţia funcţiei. A treia formă este
folosită în cazul funcţiilor care nu returnează nicio valoare prin numele lor (poate
chiar să lipsească). Dacă este prezentă, efectul ei este încheierea execuţiei funcţiei.
Tipul expresiei din instrucţiunea return trebuie să coincidă cu tipul
funcţiei.

În limbajul C nu este admisă imbricarea, adică definirea unui subprogram


în cadrul altui subprogram şi nu sunt permise salturi cu instrucţiunea goto
(instrucţiune de salt necondiţionat) în afara subprogramului.
Declararea unui subprogram apare, în cadrul fişierului sursă, înaintea
primului apel. Există cazuri particulare în care, fie funcţiile se apelează unele pe
altele (de exemplu, cazul recursivităţii mutuale), fie definiţia nu se află în fişierul
sursă. Pentru a oferi compilatorului posibilitatea să efectueze verificarea validităţii
apelurilor, sunt prevăzute declaraţii ale subprogramelor fără definire. Aceste
declaraţii se numesc prototipuri şi apar în afara oricărui corp de funcţie. Sintaxa
generală este:
tip nume ([lista-parametri-formali]);

Prototipul este de fapt un antet de funcţie după care se scrie caracterul;


(punct şi virgulă). Numele parametrilor pot lipsi, fiind suficientă specificarea
tipurilor lor. Prototipul trebuie inserat în program înaintea primului apel al funcţiei.
Domeniul de valabilitate a declaraţiei unui subprogram este limitat la partea care
urmează declaraţiei din fişierul sursă.
Prototipurile funcţiilor standard se află în fişiere header (cu extensia .h).
Utilizarea unei funcţii din bibliotecă impune includerea fişierului asociat, cu
directiva #include.

14
Programarea calculatoarelor

Fiind funcţii, subprogramele C se apelează ca operanzi în expresii, prin


numele funcţiei urmate de lista parametrilor reali. Expresia care conţine apelul
poate la limită să conţină un singur operand şi chiar să fie o instrucţiune de tip
expresie. În aceste cazuri valoarea returnată de funcţie se pierde, nefiind folosită în
niciun fel.

Exemple: Să se scrie o funcţie care calculează cel mai mare divizor comun dintre
două numere întregi nenule, utilizând algoritmul lui Euclid şi un apelant pentru
testare.
#include <stdio.h>
/*definirea functiei cmmdc*/
int cmmdc(int a, int b)
{ int r,d=a,i=b;
do {r=d%i;
d=i; i=r;}
while(r<>0);
return i;}
void main()
{ int n1,n2;
printf("Numerele pentru care se va calcula cmmdc:");
scanf("%d%d",&n1,&n2);
if(n1&&n2) printf("\ncmmdc=%d",cmmdc(n1,n2));
else printf("Numerele nu sunt nenule!"); }

Acelaşi exemplu folosind un prototip pentru funcţia cmmdc:

#include <stdio.h>
/* prototipul functiei cmmdc*/
int cmmdc(int, int);

void main()
{ int n1,n2;
printf("Numerele pentru care se va calcula cmmdc:");
scanf("%d%d",&n1,&n2);
if(n1&&n2) printf("\ncmmdc=%d",cmmdc(n1,n2));
else printf("Numerele nu sunt nenule! ");
}
/*definirea functiei cmmdc*/
int cmmdc(int a, int b)
{ int r,d=a,i=b;
do {r=d%i;
d=i; i=r;}
while(r<>0);
return i;
}

15
Subprograme

2.2 Transferul datelor între apelant şi apelat


În practica programării, s-au conturat două posibilităţi de transfer al datelor
între apelant şi apelat: prin parametri şi prin variabile globale. Prin utilizarea
variabilelor globale nu se face un transfer propriu-zis, ci se folosesc în comun
anumite zone de memorie.

2.2.1 Transferul prin parametri

Principial, transferul se poate face prin valoare sau prin adresă. În limbajul
C este implementat numai transferul prin valoare (valoarea parametrului real este
copiată în stivă, iar subprogramul lucrează numai cu această copie). Operaţiile
efectuate asupra unui parametru formal scalar (care nu este masiv) nu modifică, la
ieşirea din subprogram, parametrul real corespunzător.
Transferul valorii este însoţit de eventuale conversii de tip realizate pe
baza informaţiilor de care dispune compilatorul despre subprogram. Dacă
prototipul precede apelul subprogramului şi nu există o sublistă variabilă de
parametri, conversiile se fac similar atribuirilor.

Exemplu:
tip_returnat nume(tip_parametru p); Ù p este transferat prin valoare

Folosind transferul prin valoare se pot transmite numai parametri de intrare


în subprogram. Pentru a putea folosi parametri de ieşire trebuie simulat transferul
prin adresă. În acest scop, se vor efectua explicit operaţiile care se fac automat la
transferul prin adresă din alte limbaje: se transmite ca parametru adresa
parametrului real, iar în subprogram se lucrează cu indirectare.

Exemplu:
tip_returnat nume(tip_parametru *p); Ù p este transferat prin valoare,
fiind adresa parametrului real.

Pentru parametrii de tip masiv, simularea transferului prin adresă se face în


mod implicit, datorită modului de construire a masivelor în C: numele masivului
este un pointer. La apel, în stivă se va transfera adresa masivului iar referirea
elementelor se face automat prin calcul de adrese (vezi capitolul Tipuri dinamice
de date. Pointeri). Următoarele prototipuri sunt echivalente:

tip_returnat nume1(float v[], int n);


tip_returnat nume2(float *v, int n);

Exemple:
1. Să se calculeze produsul scalar dintre doi vectori.
a) rezultatul se întoarce prin numele funcţiei:

16
Programarea calculatoarelor

float ps(float x[], float y[], int n)


{ int i,prod=0;
for(i=0;i<n;prod+=x[i]*y[i++]);
return prod;
}

Apelul se realizează astfel:


float a[30],b[30];
int dimensiune;
………………
printf("Produsul scalar al vectorilor a si b este:%f",
ps(a,b,dimensiune));

b) rezultatul se întoarce prin parametru, simulând transferul prin adresă:


void ps(float x[], float y[], int n, float *prod)
{ int i;
*prod=0;
for(i=0;i<n;(*prod)+=x[i]*y[i++]);
}

Apelul se realizează astfel:


float a[30],b[30],produs_scalar;
int dimensiune;
ps(a,b,dimensiune,&produs_scalar);
printf("Produsul scalar al vectorilor a si b este:%f",
produs_scalar);

2. Să se calculeze elementul maxim dintr-un vector şi poziţiile tuturor


apariţiilor acestuia (v, n sunt parametri de intrare; max, nr_ap, poz sunt parametri
de ieşire).
void maxim(float v[],int n,float *max,int *nr_ap,int
poz[])
{ int i;
for(*max=v[0],i=1;i<n;i++)
if(*max<v[i])
{*nr_ap=1;poz[0]=i; max=v[i];}
else if(*max==v[i])poz[*nr_ap++]=i;
}

Apelul se realizează astfel:


float a[30],el_max;
int dimensiune,nr_aparitii,pozitii[30];
maxim(a,dimensiune,&max,&nr_aparitii,pozitii);
Antetul subprogramului este echivalent cu construcţia
void maxim(float *v, int n, float *max, int *nr_ap, int *poz)

pentru care corpul subprogramului este acelaşi.

17
Subprograme

3. Să se calculeze produsul a două matrice statice.


void produs(float a[][10],float b[][20], float c[][20],int m,
int n,int p)
{ int i,j,k;
for(i=0;i<m;i++)
for(j=0;j<p;j++)
for(c[i][j]=0,k=0;k<n;k++)c[i][j]+=a[i][k]*b[k][j];}
Observaţie:
Deşi un tablou nu poate fi returnat ca tip masiv prin numele unei
funcţii, se pot scrie funcţii care returnează prin nume un tablou ca pointer –
deoarece numele tabloului este echivalent în C cu adresa sa (pointer la începutul
masivului). Unui astfel de masiv i se alocă memorie în funcţia care îl calculează.
Numele său este returnat ca pointer la primul element al tabloului.
Exemple:
1. Să se calculeze produsul dintre o matrice şi un vector.
#include<malloc.h>
……………………
float * prod(float a[][30], float v[],int m, int n)
{ float *p;int i,j;
p=(float *)malloc(sizeof(float)*m);
for(i=0;i<m;i++)
for(p[i]=0,j=0;j<n;j++) p[i]+=a[i][j]*v[j];
return p;
}

Apelul se realizează astfel:


a)
float a[20][30], b[30], *c;
int m,n;
…………………………
c=prod(a,b,m,n);
Cu vectorul c se lucrează în modul obişnuit: elementele se referă prin indexare
(c[i], i=0..m ).
b)
float a[20][30], b[30];
int m,n;
…………………………
Se lucrează cu „vectorul” prod(a,b,m,n) – elementele sale se referă ca
prod(a,b,m,n)[i], i=0..m.
Observaţie:
La fiecare referire de element se apelează şi se execută funcţia, ceea ce
duce la consum mare şi inutil de resurse. Este preferabilă prima variantă.

2. Să se realizeze un program C pentru ridicarea unei matrice la o putere. Pentru


aceasta se folosesc două funcţii care returnează, prin pointeri, produsul a două
matrice (înmulţire), respectiv ridicarea unei matrice la o putere (putere).

18
Programarea calculatoarelor

#include<stdio.h>
#include<conio.h>
#include<alloc.h>

float** inmultire(float **a,float **b,int n)


{ int i,j,k; float **c;
c=(float **)malloc(n*sizeof(float *));
for(i=0;i<n;i++)
*(c+i)=(float *)malloc(n*sizeof(float));
for(i=0;i<n;i++)
for(j=0;j<n;j++)
for(k=0,c[i][j]=0;k<n;c[i][j]+=a[i][k]*b[k++][j]);
return c;
}

float** putere(float **a,int p,int n)


{ float **c,**ap;int l,m,i;
ap=(float **)malloc(n*sizeof(float *));
for(i=0;i<n;i++)
*(ap+i)=(float *)malloc(n*sizeof(float));
for(l=0;l<n;l++)
for(m=0;m<n;ap[l][m]=a[l][m],m++);
for(i=0;i<p-1;i++)
{c=inmultire(a,ap,n);
for(l=0;l<n;l++)
for(m=0;m<n;ap[l][m]=c[l][m],m++);
}
return ap;
}

void main()
{ int i,j,p,n,l,m; float **a,**ap,f;
clrscr();
printf("\n n=");
scanf("%i",&n);
a=(float **)malloc(n*sizeof(float *));
for(i=0;i<n;i++)
*(a+i)=(float *)malloc(n*sizeof(float));
for(i=0;i<n;i++)
for(j=0;j<n;j++)
{scanf("%f ",&f);
*(*(a+i)+j)=f;
}
scanf("%i",&p);
ap=putere(a,p,n);
for(i=0;i<n;i++)
{for(j=0;j<n;j++)
printf("%f ",*((*(ap+i)+j)));
printf("\n");
}
getch();
}

19
Subprograme

Simularea transmiterii parametrilor prin adresă

Limbajul C permite transmiterea parametrilor numai prin valoare (la apelul


subprogramelor se copiază în stivă valoarea parametrului real şi subprogramul
lucrează cu această copie). Subprogramul nu poate modifica valoarea parametrului
din apelator.
Dacă parametrul formal este un masiv, el este de fapt un pointer (adresa de
început a masivului). Folosind această proprietate, se pot modifica valorile
elementelor masivului, iar modificările se vor propaga în blocul apelator, deoarece
valoarea care se copiază în stivă este adresa de început a masivului. Masivul
rămâne în memoria principală şi poate fi modificat prin intermediul adresei sale de
început. Astfel se poate simula transmiterea parametrilor prin adresă folosind
pointerii. Subprogramul poate modifica valori care să se propage în apelator. În
acest scop se transmite ca parametru un pointer spre variabila cu care trebuie să
lucreze subprogramul apelat, care va lucra în mod explicit cu pointerul. Un
exemplu în acest sens este funcţia de citire a datelor de la tastatură. Parametrii
acestei funcţii sunt adresele variabilelor ale căror valori trebuie citite.

Exemplu:
1. Fie un subprogram care calculează suma elementelor unui vector v de lungime n.

void suma(float s, float v[], int n)


{ int i;
for(s=0,i=0;i<n;i++)
s+=v[i];
}

Subprogramul suma calculează suma elementelor vectorului, dar nu poate fi


folosită de apelator deoarece valoarea sumei este cunoscută numai în interiorul
funcţiei (parametrul a fost transmis prin valoare). În apelator valoarea variabilei
corespunzătoare parametrului formal s nu va fi modificată. Pentru ca subprogramul
să fie utilizabil, trebuie ca parametrul s să fie un pointer spre variabila în care se va
memora suma elementelor vectorului:

void suma(float* s, float v[], int n)


{ int i;
for(s=0,i=0;i<n;i++) *s+=v[i];
}

La apelul funcţiei, primul parametru actual este adresa variabilei în care se


memorează suma:
void main()
{ float x, m[20]; int n;
//…
suma(&x, m, n);
//…
}

20
Programarea calculatoarelor

2. Să se realizeze un subprogram care citeşte de la tastatură o valoare întreagă care


aparţine unui interval dat.
void citire(int a, int b, int* x)
{ do
printf("Introduceti numarul: ");
scanf("%d", x);
while((*x<=a)||(*x>=b));
}

2.2.2 Transferul prin variabile globale

Variabilele globale se declară în afara funcţiilor. Ele pot fi referite din orice
alte funcţii. De aceea, schimbul de valori între apelant şi apelat se poate realiza prin
intermediul lor. Variabilele declarate într-o funcţie se numesc locale (din clasa
automatic) şi pot fi referite numai din funcţia respectivă. Domeniul de valabilitate
a unei variabile locale este blocul (funcţia sau instrucţiunea compusă) în care a fost
definită.
Exemplu:
#include <stdio.h>
int a;
float z(char b)
{ int b;
……………
}

main()
{ int c;
……………
{ /* instructiunea compusa r */
int d;
……………
}
}

Domeniile de valabilitate a referirilor variabilelor declarate sunt: b poate fi


referit doar în funcţia z; c poate fi referit doar în funcţia main; d poate fi referit
doar în instrucţiunea compusă r; a este globală şi poate fi referită de oriunde.

2.3 Pointeri spre funcţii


În limbajul C, numele unei funcţii este un pointer care indică adresa de
memorie unde începe codul executabil al funcţiei. Aceasta permite transmiterea
funcţiilor ca parametri în subprograme precum şi lucrul cu tabele de funcţii. În
acest scop trebuie parcurse următoarele etape:
a. Declararea unei variabile de tip procedural (pointer spre funcţie):
tip_rezultat (*nume_var)(lista_parametri_formali);

21
Subprograme

unde nume_var este o variabilă de tip procedural şi are tipul pointer spre funcţie cu
parametrii lista_parametri_formali şi care returnează o valoare de tipul
tip_rezultat. Lui nume_var i se poate atribui ca valoare doar numele unei funcţii de
prototip:
tip_rezultat nume_f(lista_parametrilor_formali);

b. Descrierea funcţiei care utilizează parametrii procedurali:

void f(…,tip_rezultat (*nume)(lista_parametrilor_formali),…)


{ tip_rezultat x;

x=(*nume)(lista_parametrilor_actuali); …}

unde nume este parametrul formal de tip procedural.

c. Apelul funcţiei cu parametri procedurali:

tip_rezultat nume_functie(lista_parametrilor_formali)
{ … }
void main()
{ …
f(…, nume_functie, …); }

Exemplu: Fie o funcţie care efectuează o prelucrare asupra unui vector. Nu se


cunoaşte apriori tipul prelucrării, aceasta fiind descrisă de o altă funcţie, primită ca
parametru. Pot exista mai multe funcţii care descriu prelucrări diferite asupra unui
vector şi oricare din ele poate fi transmisă ca parametru.

float suma(float *v, int n)


{ for(int i=0, float s=0; i<n; i++)
s+=v[i];
return(s);
}

float media(float *v, int n)


{ for(int i=0, float m=0; i<n; i++)
m+=v[i];
m/=n;
return(m);
}

void functie( float vec[],int dim,float(* prelucrare)(float*, int))


{printf("Rezultatul prelucrarii este: %5.2f\n",(*prelucrare)(vec,dim));
}

Apelul se realizează prin transmiterea ca parametru real a funcţiei potrivite


prelucrării dorite.

22
Programarea calculatoarelor

void main()
{ float tab[10]; int m,i;
printf("Numarul de elemente(<10): ");
scanf("%d ", &m);
for(i=0,i<m;i++)
{printf("a(%d)=",i);
scanf("%f",&tab[i]);
}
printf("Se calculeaza suma elementelor…\n");
functie(tab, m, suma);
printf("Se calculeaza media elementelor…\n");
functie(tab, m, media);
return;
}

Limbajul C permite lucrul cu variabile de tip pointer, care conţin adresa de


început a unei funcţii (a codului său executabil). Aceste variabile permit transferul
adresei funcţiei asociate ca parametru, precum şi apelul funcţiei prin intermediul
pointerului său.
Următoarea declaraţie defineşte pointer_f ca pointer spre funcţia cu
rezultatul tip_returnat şi parametrii parametri.
tip_returnat (*pointer_f)([parametri])

Observaţie: Nu trebuie să se confunde un pointer la o funcţie cu o funcţie care are ca


rezultat un pointer, cu sintaxa de forma tip_returnat *pointer_f([parametri]).
Adresa unei funcţii se obţine prin simpla specificare a identificatorului
acesteia (fără specificarea parametrilor sau parantezelor) şi poate fi atribuită unui
pointer spre funcţie cu rezultat şi parametri compatibili. Pointerul poate fi folosit
ulterior pentru apelul funcţiei sau transmis ca parametru real în apelul unui subprogram
care conţine, în lista parametrilor formali, un pointer la un prototip de funcţie
compatibilă.
Exemple:
1. Să se aproximeze soluţia unei ecuaţii de forma f(x)=0 prin metoda
bisecţiei.
#include<stdio.h>
#include<conio.h>
#include<math.h>

/*prototipul functiei bisectie*/


void bisectie(float,float,float(*f)(float),float,long,int *,float *);

/*prototipul functiei pentru care se aplica metoda bisectiei*/


float fct(float);

/* functia principala*/
void main()
{ float a,b,eps,x;

23
Subprograme

int cod;
long n;
float (*functie)(float);
clrscr();
printf("Introduceti capetele intervalului:");
scanf("%f%f",&a,&b);
printf("\nEroarea admisa:");
scanf("%f",&eps);
printf("\nNumarul maxim de iteratii:");
scanf("%li",&n);
functie=fct;
bisectie(a,b,functie,eps,n,&cod,&x);
if(!cod)
printf("\nNu se poate calcula solutia aproximativa");
else
printf("\n Solutia aproximativa este: %f",x);
}

/*descrierea functiei pentru care se aplica metoda bisectiei*/


float fct(float x)
{ return x*x*x-3*x+14;
}

/*functia ce implementeaza metoda bisectiei*/


void bisectie(float a,float b,float (*f)(float),float eps,long n,
int *cod,float *x)
{ int gata=0;
long c;
for(c=0;(c<n)&&!gata;c++)
{ *x=(a+b)/2;
gata=fabs(*x-a)<eps;
if ((*f)(*x)*(*f)(a)<0)
b=*x;
else a=*x;
}
*cod=gata;
}

2. Să se sorteze un şir cu elemente de un tip neprecizat, dar pe care se poate


defini o relaţie de ordine (de exemplu numeric, şir de caractere, caracter).
Metoda aleasă spre exemplificare este sortarea prin selecţie directă. Un
subprogram de sortare care să nu depindă de tipul elementelor şi de criteriul de
sortare considerat trebuie să aibă ca parametri formali:
- vectorul de sortat, ca pointer la tipul void, asigurându-se astfel posibilitatea realizării
operaţiei de schimbare a tipului („cast”) în funcţie de necesităţile ulterioare (la momentul
apelului se poate realiza modificarea tipului void * în tip_element *, unde tip_element
reprezintă tipul elementelor vectorului de sortat);
- dimensiunea vectorului de sortat şi numărul de octeţi din reprezentarea tipului
elementelor vectorului;
- pointerul la o funcţie de comparare, cu argumente de tip void *, care să
permită la apel atât schimbarea de tip, cît şi descrierea efectivă a relaţiei de ordine.

24
Programarea calculatoarelor

Cum tipul elementelor vectorului nu este cunoscut la momentul descrierii


procedurii de sortare, operaţia de atribuire nu poate fi folosită, ea fiind înlocuită de
o funcţie de copiere a unui număr prestabilit de octeţi, de la o adresă sursă la una
destinaţie. O astfel de funcţie există în biblioteca mem.h, sintaxa ei fiind:
void *memmove(void *destinaţie, const void *sursă, unsigned n)

Pentru accesarea adresei elementului de rang i din vector se foloseşte


formula (char *)v+i*nr_octeti. Fişierul sursă care conţine funcţia de sortare
descrisă anterior este:
//fisier exp_tip.cpp
#include <mem.h>
include<alloc.h>

void sort(void *v, int n, int dim,


int (*compara)(const void * ,const void * ))
{ int i,j;
void *aux;
aux=malloc(dim);
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if((*compara)((char*)v+dim*i,(char*)v+dim*j))
{ memmove(aux,(char*)v+dim*i,dim);
memmove((char*)v+dim*i,(char*)v+dim*j,dim);
memmove((char*)v+dim*j,aux,dim);
}
free(aux);
}

Exemplu de apel pentru un vector de numere reale:


#include <stdio.h>
#include<conio.h>
#include "exp_tip.cpp"

int compara(const void *a, const void *b)


{ if(*(float *)a>*(float *)b)return 1;
else return 0;
}
void main()
{ float vect[20]; int n,i;
clrscr();
printf("Dimensiunea vectorului:");
scanf("%d",&n);
printf("\nElementele:");
for(i=0;i<n;i++)
scanf("%f",&vect[i]);
sort(vect,n,sizeof(float),compara);
printf("\nElementele sortate:");
for(i=0;i<n;i++)
printf("\n%f",vect[i]);
getch();
}

25
Subprograme

Exemplu de apel pentru un vector de cuvinte (şiruri de caractere):


#include <stdio.h>
#include <string.h>
#include<conio.h>
#include "exp_tip.cpp"
int compara(const void *a, const void *b)
{ if(strcmp((char *)a, (char *)b)>0)return 1;
else return 0;
}
void main()
{ typedef char cuvant[10];
cuvant vect[20];
int n;
clrscr();
printf("Dimensiunea vectorului de cuvinte:");
scanf("%d",&n);
printf("\nCuvintele:");
for(int i=0;i<n;i++)
scanf("%s",&vect[i]);
sort(vect,n,10,compara);
printf("\nCuvintele sortate:");
for(i=0;i<n;i++)
printf("\n%s",vect[i]);
getch();}

2.4 Funcţii cu număr variabil de parametri


Bibliotecile limbajului C conţin subprograme standard cu număr variabil
de parametri. Limbajul C permite definirea funcţiilor utilizator cu număr variabil
de parametri, prin utilizarea unui set de macrodefiniţii, declarate în biblioteca
stdarg.h, care permit accesul la lista de parametri.
Fişierul stdarg.h declară tipul va_list şi funcţiile va_start, va_arg şi
va_end, în care:
- va_list este un pointer către lista de parametri. În funcţia utilizator
corespunzătoare trebuie declarată o variabilă (numită în continuare ptlist) de acest
tip, care va permite adresarea parametrilor;
- va_start iniţializează variabila ptlist cu adresa primului parametru din sublista
variabilă. Prototipul acestei funcţii este:
void va_start(va_list ptlist, ultim)

unde ultim reprezintă numele ultimului parametru din sublista variabilă. În unele
situaţii (vezi exemplele) se transferă în acest parametru numărul de variabile
trimise.

26
Programarea calculatoarelor

- va_arg întoarce valoarea parametrului următor din sublista variabilă. Prototipul


acestei funcţii este:
tip_element va_arg(va_list ptlist, tip_element)

unde tip_element este tipul elementului transferat din listă. După fiecare apel al
funcţiei va_arg, variabila ptlist este modificată astfel încât să indice următorul
parametru.
- va_end încheie operaţia de extragere a valorilor parametrilor şi trebuie apelată
înainte de revenirea din funcţie. Prototipul funcţiei este:

void va_end(va_list ptlist)

Problema numărului de parametri şi tipurilor lor este tratată de programator.

Exemple:
1. Să se calculeze cel mai mare divizor comun al unui număr oarecare de numere
întregi.
#include<stdio.h>
#include<conio.h>
#include<stdarg.h>

int cmmdc_var(int,...);
int cmmdc(int, int);

void main()
{ int x,y,z,w;
clrscr();
scanf("%d%d%d%d",&x,&y,&z,&w);
printf("\nCmmdc al primelor 3 numere:%d\n",cmmdc_var(3,x,y,z));
printf("\nCmmdc al tuturor numerelor:%d\n",cmmdc_var(4,x,y,z,w));
}

//cel mai mare divizor comun a doua numere


int cmmdc(int x,int y)
{ int d=x,i=y,r;
do{ r=d%i;
d=i;i=r;
}
while(r);
return d;
}

//cel mai mare divizor comun a nr numere


int cmmdc_var(int nr,...)
{ va_list ptlist;
/*initializarea lui ptlist cu adresa de inceput a listei de
parametri*/
va_start(ptlist,nr);
//extragerea primului parametru, de tip int
x=va_arg(ptlist,int);
for(int i=1;i<nr;i++)

27
Subprograme

{ //extragerea urmatorului element din lista de parametri


y=va_arg(ptlist,int);
z=cmmdc(x,y);x=z;
}
va_end(ptlist);
return x;
}

2. Să se interclaseze un număr oarecare de vectori.


Spre deosebire de exemplul anterior, în care în lista de parametri a funcţiei cu
număr oarecare de parametri figurau elemente de acelaşi tip (int), acest exemplu
ilustrează modul de transfer şi acces la elemente de tipuri diferite. Funcţiei
intre_var i se transmit la apel vectorul rezultat, iar pentru fiecare vector de
interclasat, adresa de început (pointer la tipul double) şi numărul de elemente (int).
Numărul parametrilor din lista variabilă este, în acest, caz 2*numărul de vectori de
interclasat.
#include<stdarg.h>
#include<stdio.h>
#include<conio.h>

void inter(double *,int,double *,int,double *);


void inter_var(double *,int nr,...);

void main()
{ int n1,n2,n3,n4;
double x1[10],x2[10],x3[10],x4[10],z[50];
clrscr();
scanf("%d%d%d%d",&n1,&n2,&n3,&n4);
for(int i=0;i<n1;i++)
scanf("%lf",&x1[i]);
for(i=0;i<n2;i++)
scanf("%lf",&x2[i]);
for(i=0;i<n3;i++)
scanf("%lf",&x3[i]);
for(i=0;i<n4;i++)
scanf("%lf",&x4[i]);
inter_var(z,4,x1,n1,x2,n2);
printf("\nRezultatul interclasarii primilor 2 vectori\n");
for(i=0;i<n1+n2;i++)
printf("%lf ",z[i]);
inter_var(z,8,x1,n1,x2,n2,x3,n3,x4,n4);
printf("\nRezultatul interclasarii celor 4 vectori\n");
for(i=0;i<n1+n2+n3+n4;i++)
printf("%lf ",z[i]);
}

28
Programarea calculatoarelor

void inter(double *x, int n1, double *y, int n2, double *z)
{ int i,j,k;
for(i=0,j=0,k=0;(i<n1)&&(j<n2);k++)
if(x[i]<y[j])
z[k]=x[i++];
else z[k]=y[j++];
if(i<n1)
for(;i<n1;z[k++]=x[i++]);
else for(;j<n2;z[k++]=y[j++]);
}

void inter_var(double *z,int nr,...)


{ va_list ptlist;
double *x,*y,x1[100];
int n1,n2;
/*initializarea lui ptlist cu adresa de inceput a listei de
parametri*/
va_start(ptlist,nr);
//extragerea primului vector
x=va_arg(ptlist,double *);
//extragerea dimensiunii lui
n1=va_arg(ptlist,int);
for(int j=0;j<n1;j++)x1[j]=x[j];
for(int i=1;i<(int)(nr/2);i++)
{ //extragerea urmatorului vector
y=va_arg(ptlist,double *);
//extragerea numarului sau de elemente
n2=va_arg(ptlist,int);
inter(x1,n1,y,n2,z);
for(j=0;j<n1+n2;j++)
x1[j]=z[j];n1+=n2;
}
va_end(ptlist);
}

29
3 Subprograme recursive

Recursivitatea este tehnica de programare în care un subprogram se auto-


apelează. Limbajul C face parte din clasa limbajelor de programare care admit
scrierea de funcţii recursive. În continuare sunt prezentate câteva exemple simple
de subprograme C prin intermediul cărora sunt calculate recursiv valorile
n! , C nk , f ο g ο f , unde n , k ∈ N şi f, g funcţii, f , g : R → R. De asemenea, este
ilustrată maniera în care sunt efectuate apelurile recursive şi tratarea condiţiilor
terminale.

3.1 Calcul recursiv


Calculul valorii n! pentru n dat poate fi efectuat pe baza formulei recursive
⎧1, n = 0
n! = ⎨ .
⎩n(n − 1)! , n > 0
Fie fact(n) funcţia C care calculează n!. 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
decrementată. Cazurile în care este posibilă evaluarea „imediată” se numesc
condiţii terminale.
În limbajul C, funcţia fact este

long fact(unsigned n)
{ if (!n) return 1;
return n*fact(n-1);
}
n!
Utilizarea formulei C nk = pentru calculul combinărilor
k ! (n − k )!
( n , k ∈ N date) este ineficientă şi uneori imposibilă deoarece n!, pentru n ≥ 13 nu

30
Subprograme recursive

poate fi reprezentat în calculator ca dată de un tip întreg, chiar dacă numărul C nk


este relativ mic şi poate fi reprezentat prin intermediul unui tip întreg. Pe baza
relaţiei de recurenţă C nk = C nk−1 + C nk−−11 , valoarea C nk poate fi calculată astfel. Fie
comb(n,k) funcţia care 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(n-1,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:

long comb(unsigned n, unsigned k)


{ if (k>n) return 0;
if ((k==0)||(k=n)) return1;
return comb(n-1,k)+comb(n-1,k-1);
}

Funcţiile C recursive pentru calculul n! , C nk , unde n , k ∈ N realizează


apeluri recursive directe. Schema unui apel recursiv poate fi descrisă astfel: se
verifică dacă este îndeplinită cel puţin una din condiţiile terminale; dacă este
îndeplinită o condiţie terminală, atunci calculul este încheiat şi controlul este
returnat unităţii apelante, î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ă a valorilor parametrilor formali, a adresei de revenire şi a variabilelor locale.
La momentul execuţiei, aceste informaţii sunt extrase cu eliminare din stivă,
eliberându-se spaţiul ocupat.
În cazul subprogramelor recursive, mecanismul funcţionează astfel: este
generat un număr de apeluri succesive cu ocuparea spaţiului din stivă necesar
efectuării apelurilor până la îndeplinirea unei condiţii terminale; apelurile sunt
executate în ordinea inversă celei în care au fost generate, iar operaţia de inserare
în stivă poate produce depăşirea spaţiul de memorie rezervat.
De exemplu, în cazul apelului fact(3), secvenţa de apeluri recursive 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 3.1.a
şi 3.1.b, unde (○) reprezintă adresa de revenire în punctul de unde a fost efectuat
apelul fact(3).
Apelurile recursive ale unui subprogram S1 pot fi şi indirecte, în sensul că
este efectuat un apel al unui alt subprogram S2 şi S2 iniţiază un apel al lui S1. De

31
Programarea calculatoarelor

exemplu, calculul valorilor funcţiei h=f◦g◦f , unde f,g:R→R sunt funcţii date poate
fi descris astfel. Pentru funcţiile f, g definite prin:

3 2

Fact=3*Fact(2)
Fact=2*Fact(1)

(o)
Fact=3*Fact(2)

(o)

1 0

Fact=1*Fact(0) Fact=1

2 1

Fact=2*Fact(1) Fact=1*Fact(0)

2
3

Fact=2*Fact(1)
Fact=3*Fact(2)

(o)
Fact=3*Fact(2)

(o)
Figura 3.1.a Evoluţia în stivă până la verificarea condiţiei terminale n=0

32
Subprograme recursive

1 2

Fact=1 Fact=2

2 3

Fact=2*Fact(1) Fact=3*Fact(2)

3
3

Fact=3*Fact(2)
Fact=6
(o)

(o)
Figura 3.1.b Eliberarea stivei după execuţia determinată de condiţia terminală

⎧2 x 3 + 1, x < 5
⎪ ⎧⎪5 x 2 − 3 x + 2 , x ≤ 1
f (x ) = ⎨ x 4 + 2 , 5 ≤ x < 8 , g (x ) = ⎨ 3
⎪3 , x > 8 ⎪⎩ x − x + 5 , x > 1

funcţiile C pentru calculul h=f◦g◦f pot fi descrise astfel,

float f(float x)
{ if (x<5) return 2*pow(x,3)+1;
if (x<8) return pow(x,4)+2;
return 3;
}

float g(float x)
{ if (x<=1) return 5*x*x-3*x+2;
return pow(x,3)-x+5;
}

float h(float x)
{ return f(g(f(x)));
}

33
Programarea calculatoarelor

3.2 Aplicaţii cu subprograme recursive


1. Să se scrie o funcţie C care citeşte o secvenţă oarecare de cuvinte a1, a2,
….., an terminată cu simbolul # şi afişează anan-1…a1. Pentru rezolvarea problemei
se utilizează funcţia recursivă Scrie.
void Scrie()
{ char cuvant[100];
scanf(“%s“,&cuvant);
if (strcmp(cuvant,“#“))
{ Scrie;
printf( “%s\n“,cuvant);
}
}

2. Calculul valorii funcţiei Ackermann.


Funcţia Ackermann este definită pentru argumentele m,n numere naturale
prin
⎧n + 1, m = 0

a (m , n ) = ⎨a (m − 1,1), n = 0
⎪a (m − 1, a (m , n − 1)), altfel

Funcţia C Ackermann calculează valoarea funcţiei a pentru m, n parametri
naturali daţi.

long Ackermann(unsigned m, unsigned n)


{ if (!m) return n+1;
if (!n) return Ackermann(m-1,1);
return Ackermann(m-1,Ackermann(m,n-1));
}

3. Problema calculului celui mai mare divizor comun dintre două numere
naturale a şi b poate fi rezolvată recursiv, conform definiţiei următoare,
⎧a , a = b
(a ,b ) = ⎪⎨( a − b ,b ), a > b
⎪( a ,b − a ), b > a

Funcţia C cmmdc(a,b) este,

long cmmdc(long a, long b)


{ if (a==b) return a;
if (a>b) return cmmdc(a-b,b);
return cmmdc(a,b-a);
}

34
Subprograme recursive

4. Problema turnurilor din Hanoi ilustrează foarte bine avantajele


recursivităţii. Problema poate fi enunţată astfel: se presupune că există trei tije a, b,
c, pe tija a fiind plasate n discuri de diametre diferite în ordinea descrescătoare a
acestora. Se cere ca cele n discuri de pe tija a să fie deplasate pe tija c astfel încât
să fie îndeplinite condiţiile:
• la fiecare mutare este deplasat unul dintre discurile aflate pe poziţia
superioară pe una din tije;
• oricare din discuri poate fi aşezat numai pe un disc de diametru mai
mare;
• tija b poate fi folosită pentru deplasări intermediare.

Notând cu P(n,a,c) problema transferului celor n discuri de pe tija a pe tija


c, pentru rezolvarea ei putem raţiona în modul următor. Dacă s-a rezolvat problema
P(n-1,a,b), atunci discul de diametru maxim care se află încă pe tija a este deplasat
pe tija c şi în continuare se rezolvă problema P(n-1,b,c). Soluţia recursivă este
prezentată în funcţia Hanoi.

Exemplu
Presupunând că discurile sunt numerotate în ordinea crescătoare a
diametrelor cu etichetele 1, 2, 3, o soluţie a problemei pentru n=3 poate fi descrisă
astfel.

Tija a Tija b Tija c Mutarea efectuată


1 a⇒c
2
3
2 1 a⇒b
3
3 2 1 c⇒b
3 1 a⇒c
2
1 3 b⇒a
2
1 2 3 b⇒c
1 2 a⇒c
3
1
2
3
#include <stdio.h>
#include <conio.h>

void Hanoi(unsigned n,unsigned a, unsigned b,unsigned c)


{

35
Programarea calculatoarelor

if(n>0)
{ Hanoi(n-1,a,c,b);
printf("Transfer disc de pe tija %u pe tija %u\n",a,b);
Hanoi(n-1,c,b,a); }
}
void main()
{ unsigned n,a,b,c;
clrscr();
printf("n=");scanf("%u",&n);
Hanoi(n,1,2,3);getch();
}

5. Căutarea în vectori sortaţi (căutarea binară)


Fie v este un vector de numere reale sortat crescător şi k este un număr real
dat. Problema este de a identifica (dacă există) o valoare poz, astfel încât v[poz]=k.
Rezolvarea ei porneşte cu luarea în considerare a întregului vector v şi
⎡n⎤
determinarea poziţiei mijlocului m = ⎢ ⎥ . Dacă v[m]=k, atunci poz:=m. Dacă
⎣2⎦
v[m]>k, atunci se procedează în acelaşi mod cu vectorul format din primele m
componente din v, altfel cu cel format din componentele v[m+1],…,v[n-1]. Se
generează astfel subvectori din ce în ce mai mici până când valoarea k este găsită
sau până când nu mai poate fi generat un nou subvector.

#include <stdio.h>
#include <conio.h>
int cauta_binar(float *,int,int,float);

void main()
{ clrscr();
printf("Dimensiunea vectorului:");
int n;
scanf("%i",&n);
printf("Elementele vectorului\n");
float v[100];
for(unsigned i=0;i<n;i++)
scanf("%f",&v[i]);
printf("Cheia de cautare:");
float k;
scanf("%f",&k);
int c=cauta_binar(v,0,n-1,k);
if(c==-1)
printf("Cheia nu a fost gasita");
else printf("Cheia pe pozitia:%i",c);
getch();
}

int cauta_binar(float *v,int li,int ls,float k)


{ if(li>ls)
return -1;
int mij=(li+ls)/2;
if(v[mij]==k)

36
Subprograme recursive

return mij;
if(v[mij]>k)
return cauta_binar(v,li,mij-1,k);
return cauta_binar(v,mij+1,ls,k);
}

6. Sortarea crescătoare prin inserare


Pentru sortarea crescătoare a unei secvenţe de numere reale se poate raţiona
astfel: dacă P(n) este problema sortării crescătoare a secvenţei a1, a2,…,an şi P(n-1)
este problema sortării primelor n-1 componente, atunci soluţia problemei P(n)
rezultă din soluţia problemei P(n-1) prin inserarea lui an în soluţia problemei
P(n-1). Fiecare problemă intermediară P(k), k = 2 ,..., n este rezolvată aplicând
aceeaşi metodă P(1) fiind o problemă „gata rezolvată” (condiţie terminală).
Funcţia insera realizează inserarea valorii x în vectorul v în poziţia
„corectă”. Funcţia recursivă inssort realizează sortarea vectorului cu n componente
prin inserţie.

void insera(float *v,int *n,float x)


{ for(int i=0;(i<*n)&&(x>v[i]);i++);
for(int j=*n;j>=i+1;j--)v[j]=v[j-1];
v[i]=x;(*n)++;
}

void inssort(float *v,int n)


{ if(n)
{ inssort(v,n-1);int m=n-1;
insera(v,&m,v[n-1]);
}
}

7. Pot fi realizate desene prin compunerea într-o manieră recursivă a unor


figuri geometrice primitive. Compunerea constă în repetarea primitivelor
considerate şi a rezultatelor obţinute prin rotirea lor într-un sens sau celălalt. Astfel,
dacă mulţimea de primitive H0 constă dintr-un punct şi pentru compunere este
considerat un segment de lungime h, atunci: H1 rezultă din patru exemple (cópii,
realizări, instanţe, clone) de primitive din H0 unite prin segmente de lungime h; H2
rezultă din 16 exemple din H0 unite prin 15 segmente de lungime h/2 ş.a.m.d. De
asemenea, H2 se poate obţine prin interconectarea a patru cópii ale lui H1 rotite cu
unghiuri drepte şi prin interconectarea punctelor izolate prin segmente de aceeaşi
lungime. Generalizând, o curbă Hn rezultă din patru cópii ale unei curbe Hn-1,
punctele izolate fiind unite prin segmente de lungime hn=h/2n. Curbele rezultate se
numesc curbele Hilbert Hi, i ≥ 0.

37
Programarea calculatoarelor

H1 H2 H3

Dacă cele patru părţi ale unei curbe Hilbert Hk sunt notate A, B, C, D şi se
reprezintă prin săgeţi rutinele care desenează segmentele care le interconectează,
atunci rezultă următoarele scheme recursive.

A: D ← A↓ A→ B

A: D ← A↓ A→ B
B: C↑B→B↓ A
C: B→C ↑C ← D
D: A↓ D← D↑C
Prin executarea următoarei surse C sunt obţinute curbele Hilbert H4.
#include <stdio.h>
#include <graphics.h>
#include <stdlib.h>
#include <conio.h>
const n=5;
const h0=480;

int i=0;
int h;
int x,y,x0,y0,gm;
int gd=DETECT;
void A(int);
void B(int);
void D(int);
void C(int);
void main()
{ clrscr();
initgraph(&gd,&gm,"D:\BC\BGI");
setbkcolor(0);
setcolor(4);
h=h0;y0=x0=h/2;
do{ i++;h/=2;
x0+=h/2;y0+=h/2;
x=x0;y=y0;moveto(x,y);
A(i); }

38
Subprograme recursive

while(i<n);
getch();
closegraph();
}
void A(int i)
{ if (i>0)
{ D(i-1);x-=h;lineto(x,y);
A(i-1);y-=h;lineto(x,y);
A(i-1);x+=h;lineto(x,y);
B(i-1);
}
}
void B(int i)
{ if (i>0)
{ C(i-1);y+=h;lineto(x,y);
B(i-1);x+=h;lineto(x,y);
B(i-1);y-=h;lineto(x,y);
A(i-1);
}
}
void C(int i)
{ if (i>0)
{ B(i-1);x+=h;lineto(x,y);
C(i-1);y+=h;lineto(x,y);
C(i-1);x-=h;lineto(x,y);
D(i-1);
}
}
void D(int i)
{ if (i>0)
{ A(i-1);y-=h;lineto(x,y);
D(i-1);x-=h;lineto(x,y);
D(i-1);y+=h;lineto(x,y);
C(i-1);
}
}

39
Programarea calculatoarelor

Curba Hilbert obţinută este

8. În cazul curbelor Hilbert, toate unghiurile determinate de segmentele


care unesc punctele sunt de măsură 900. Dacă se consideră ca valori pentru
măsurile unghiurilor determinate de aceste segmente 450, 900, 1350, rezultă curbele
Sierpinski Sn, n ≥ 1.
Curba Sierpinski S2 este,

Recursia pentru obţinerea curbelor Sierpinski poate fi descrisă astfel.


S: A B C D

A: A B⇒D A

40
Subprograme recursive

B: B C⇓ A B

C: C D⇐B C

D: D A⇑ C D

unde săgeţile duble indică segmente de lungime 2h.

Următorul program desenează curbele Sierpinski S4.

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

const n=4;
const h0=412;
int i=0;
int h;
int x,y,x0,y0,gm;
int gd=DETECT;
void A(int);
void B(int);
void D(int);
void C(int);
void main()
{ clrscr();
initgraph(&gd,&gm,"d:\bc\bgi");
setbkcolor(15);
setcolor(8);
h=h0/4;
x0=2*h;
y0=3*h;
do{ i++;
x0-=h;h/=2;y0+=h;
x=x0;y=y0; moveto(x,y);
A(i);x+=h;y-=h;lineto(x,y);
B(i);x-=h;y-=h;lineto(x,y);
C(i);x-=h;y+=h;lineto(x,y);
D(i);x+=h;y+=h;lineto(x,y);}
while(i!=n);
getch();
closegraph();
}

void A(int i)
{ if (i>0)
{ A(i-1);x+=h;y-=h;
lineto(x,y);
B(i-1);x+=2*h;
lineto(x,y);

41
Programarea calculatoarelor

D(i-1);x+=h;y+=h;
lineto(x,y);
A(i-1);
}
}

void B(int i)
{ if (i>0)
{ B(i-1);x-=h;y-=h;
lineto(x,y);C(i-1);
y-=2*h;
lineto(x,y);
A(i-1);x+=h;y-=h;
lineto(x,y);
B(i-1);
}
}

void C(int i)
{ if (i>0)
{ C(i-1);x-=h;y+=h;
lineto(x,y);
D(i-1);x-=2*h;
lineto(x,y);
B(i-1);x-=h;y-=h;
lineto(x,y);
C(i-1);
}
}
void D(int i)
{ if (i>0)
{ D(i-1);x+=h;y+=h;
lineto(x,y);
A(i-1);y+=2*h;
lineto(x,y);
C(i-1);x-=h;y+=h;
lineto(x,y);
D(i-1);
}
}

Rezultatul execuţiei programului este prezentat în următoarea figură.

42
Subprograme recursive

43
4 Tipul de dată articol

Articolul este o structură de date eterogenă, cu acces direct la elementele


sale, între care există o relaţie de ordine ierarhică.

4.1 Caracteristici generale şi mod de declarare


Articolul poate fi reprezentat sub formă de arbore, ale cărui noduri sunt
asociate componentelor structurii. Componentele de pe ultimul nivel sunt scalare şi
se numesc date elementare sau câmpuri. Datele de pe celelalte niveluri, denumite
date de grup, se constituie prin agregarea datelor de pe nivelurile inferioare. Data
de grup de cel mai înalt nivel (rădăcina arborelui) corespunde articolului în
ansamblu. Conceptual, datele de grup de pe diverse niveluri au aceleaşi proprietăţi
ca şi articolul, ceea ce permite ca această structură să fie construită recursiv, prin
descompunerea în structuri cu aceleaşi proprietăţi (figura 4.1).
Declararea împreună a tipului articol şi a variabilelor de acest tip se
realizează conform sintaxei:

struct tip_articol{lista_campuri} var1,var2,…,varn;

unde tip_articol este identificatorul asociat tipului articol, iar var1, var2,…, varn
sunt identificatorii asociaţi variabilelor de tipul articol declarat.
Parametrii declaraţiei pot lipsi (dar nu toţi deodată). Dacă lipsesc
parametrii var1, var2,…, varn, atunci tip_articol trebuie să fie prezent, fiind numai
o declarare explicită de tip nou, utilizabil ulterior la alte declarări. Dacă lipseşte
tip_articol, atunci trebuie să fie prezentă lista de variabile (nevidă), caz în care este
vorba de o declarare de variabile de tip articol, fără însă a declara şi un tip utilizator
nou. În continuare, tip_articol este un tip nou de date, iar var1, var2,…, varn sunt
variabile de tipul tip_articol. Variabilele pot fi declarate şi ca masive, ale căror

44
Tipul de dată articol

elemente sunt de tip articol: var1[dim1][dim2]…[dimn].

DATA dată de grup (articol)

ZI LUNA AN date elementare

a)

PERSOANA dată de grup (articol)

dată de grup (articol)


NUME ADRESA DATA NAŞTERII

date elementare ZI LUNA AN


b)
Figura 4.1 Exemple de structuri de articole

O variabilă de tip articol poate fi declarată şi ulterior definirii tipului:


struct tip_articol var1;

Descrierea constituie o definire implicită de un nou tip de dată. Este


posibilă definirea explicită a unui nou tip de dată, adăugând cuvântul rezervat
typedef în faţa declarării (în acest caz nu mai pot fi declarate simultan şi variabile).
Lista_campuri este o înşiruire de declaraţii de câmpuri separate prin punct
şi virgulă, asemănătoare declaraţiilor de variabile, de forma tip_camp nume_camp.
Câmpurile unei structuri pot fi variabile simple, masive sau alte articole. Lista
câmpurilor nu poate fi vidă.

Exemplu: definirea tipului de dată număr complex, a unei variabile simple şi a


unui masiv unidimensional cu elemente de acest tip se poate face în oricare din
următoarele variante (pentru un număr complex se vor reţine partea reală şi partea
imaginară):
a) struct COMPLEX{float r,i;}a,b[100];
b) struct COMPLEX{float r,i;};
struct COMPLEX a,b[100];
c) struct COMPLEX{float r,i;};
COMPLEX a,b[100];
d) struct {float r,i;}COMPLEX;
COMPLEX a,b[100];
e) typedef struct {float r,i;} COMPLEX;
COMPLEX a,b[100];
f) typedef struct COMPLEX{float r,i;};
struct COMPLEX a,b[100];
g) typedef struct COMPLEX{float r,i;};
COMPLEX a,b[100];

45
Programarea calculatoarelor

Din punct de vedere practic, utilizarea tipului articol este strâns legată de
prelucrarea fişierelor. În lucrul cu variabilele de tip articol se recomandă declararea
identificatorului de tip. În acest mod, identificatorul de tip articol poate fi folosit în
definirea mai multor variabile. În procesul de descriere a unui articol, arborele se
parcurge în preordine (de la rădăcină spre extremităţi şi de la stânga la dreapta).

Exemplu: pentru exemplele din figura 4.1, declararea poate fi realizată prin
definire recursivă, astfel:
struct tip_data
{ unsigned zi;
char luna[3];
int an; };
struct persoana
{ char nume[30];
char adresa[50];
struct tip_data data_nasterii;
} angajat;

Dacă nu ar fi existat declaraţia tipului articol tip_data, atunci tipul persoana putea
fi scris astfel:
struct persoana
{ char nume[30];
char adresa[50];
struct
{ unsigned zi;
char luna[3];
int an;
} data_nasterii;
} angajat;

Variabilele de tip articol se reprezintă intern ca succesiuni de câmpuri


elementare, cu reprezentarea internă şi lungimea fizică specifice tipurilor lor.
Lungimea zonei de memorie rezervată pentru variabila de tip articol rezultă din
însumarea lungimilor câmpurilor. Aceasta nu poate depăşi 65520 octeţi (ca orice
variabilă de tip structurat). Pentru structura unui articol îşi dovedeşte utilitatea
operatorul sizeof, care asigură determinarea lungimii zonei de memorie asociate
unei variabile sau unui tip de date.

Exemplu:
Considerând declaraţiile anterioare, expresia sizeof(data_nasterii) are
valoarea 8, iar sizeof(angajat) are valoarea 90.

Din punct de vedere fizic, identificatorii câmpurilor din descrierea


articolului reprezintă deplasări faţă de începutul acestuia. Adresa fizică a unui
câmp rezultă din însumarea adresei articolului cu deplasarea sa. Structura
arborescentă a articolelor poate fi exprimată sugestiv şi prin machete, care
evidenţiază componentele, natura, lungimea declarată şi lungimea fizică ale
acestora (figurile 4.2 şi 4.3).

46
Tipul de dată articol

4.2 Referirea articolului şi a elementelor componente


Datele de tip articol pot fi referite în două moduri: global sau pe
componente. Referirea globală este permisă numai în operaţia de atribuire, cu
condiţia ca ambele variabile (sursă şi destinaţie) să fie articole de acelaşi tip.
Referirea pe componente (prin numele lor) este o reflectare a faptului că
articolul este o structură cu acces direct. Referirea unor componente de tip articol
din structura altui articol este posibilă numai în operaţia de atribuire, în condiţiile
precizate anterior la referirea globală. În cele ce urmează se are în vedere numai
referirea componentelor de tip dată elementară, situate pe ultimul nivel al structurii.
Referirea câmpurilor unei structuri se face prin calificare, folosind
operatorul. (punct). În referirea prin calificare, asigurarea identificării unice a
câmpurilor se realizează prin asocierea numelui acestora cu numele articolului care
le conţine. Construcţia rămâne la această formă în cazul în care structura are numai
două niveluri: articolul şi câmpurile elementare ale acestuia.
Exemple:
Folosind tipul COMPLEX definit anterior, avem:
a.r , a.i - se referă partea reală, respectiv imaginară a
variabilei a
b[10].r - se referă partea reală a celui de-al 11-lea element
al vectorului b

#include <string.h>
main()
{ struct articol {char nume[40];
char adresa[30];
int an, luna, zi;}
struct articol pers;
……………
strcpy(pers.nume, "Popescu Ion");
strcpy(pers.adresa, "Bucuresti, Pta. Romana 6");
pers.an=1979; pers.luna=3; pers.zi=15;
}

În articolele cu structură recursivă se realizează calificarea progresivă cu


articolele de pe nivelurile superioare, primul calificator fiind numele articolului
rădăcină. În lanţul de calificări, numele articolului rădăcină este nume de variabilă,
celelalte fiind nume de câmpuri ale articolului. Dacă anumite componente sunt
structuri de date de alte tipuri (de exemplu masive sau şiruri de caractere), în
referirea elementelor lor se aplică, pe lângă calificare, regulile specifice acestor
structuri.
Exemple:
1. Referirea prin calificare a câmpurilor articolului angajat de tipul persoana (vezi
exemplele anterioare) se realizează astfel:
angajat.nume;
angajat.adresa;
angajat.data_nasterii.zi;
angajat.data_nasterii.luna;
angajat.data_nasterii.an

47
Programarea calculatoarelor

În aceste referiri, angajat este identificatorul variabilei articol, celelalte elemente


sunt identificatori de câmpuri. Construcţiile angajat.nume şi angajat.adresa
corespund referirii globale a câmpurilor respective, care sunt şiruri de caractere.
Pentru a referi, de exemplu, primul caracter din şir, se scrie: angajat.nume[0].
2. Se presupune un articol cu structura din figura 4.2.
Cod Vînzări lunare
Magazin Luna 1 Luna 2 … Luna 12
Întreg real Real … real
2 4 4 … 4
Figura 4.2 Structura de articol pentru exemplul 2
Articolul se declară astfel:
struct magazin { int cod_magazin;
float vanzari_lunare[12];
} articol;
Articolul are 50 de octeţi, iar referirea câmpurilor se realizează astfel:
articol.cod_magazin; articol.vanzari_lunare[i], cu i=0,1,…,11.

3. Se presupune un articol cu structura din figura 4.3.


Număr Materia primă 1 ... Materia primă 30
Cod
materii Norma de Norma de
produs Cod ... Cod
prime consum consum
întreg întreg întreg real ... întreg real
2 1 2 4 ... 2 4

Figura 4.3 Structura de articol pentru exemplul 3

Cu toate că numărul de materii prime utilizate poate fi variabil de la un produs la


altul, în descrierea articolului se alege valoarea maximă a acestuia:
struct a { int cod_mat; float norma; };
struct produs { int cod_produs;
unsigned char nr_mat;
struct a materii_prime[30];
} articol ;

Articolul are 183 de octeţi, iar referirea câmpurilor se realizează astfel:


articol.cod_produs; articol.nr_mat;
articol.materii_prime[i].cod_mat; articol.materii_prime[i].norma;

Constantele de tip articol sunt cu tip şi păstrează caracteristicile acestora,


descrise în § 4.2. În momentul compilării se rezervă zone de memorie pentru
acestea, iar câmpurile articolelor sunt iniţializate cu valorile precizate de utilizator.
Declararea constantelor presupune definirea anterioară a tipului articol.
Valoarea iniţială trebuie să fie de acelaşi tip cu câmpul căruia îi corespunde.
Când articolul conţine la rândul său alt articol, identificarea câmpului care se
iniţializează se face pe niveluri, folosind perechi corespunzătoare de acolade.

48
Tipul de dată articol

Exemple:
#include <stdio.h>
void main()
{
//exemplul 1
struct persoana
{ char nume[40];
char adresa[30];
struct
{ int zi, luna, an;} datan;
};
//exemplul 2
struct magazin
{ int cod_magazin;
float vanzari_lunare[12];
};
//exemplul 3
struct a { int cod_mat; float norma;};
struct produs
{ int cod_produs;
unsigned char nr_mat;
struct a materii_prime[30];
};

//Initializarea articolului din exemplul 1:


struct persoana p={"Popescu Ion", "Bucuresti, Magheru 14", 2, 4, 1960};

//sau cu evidentierea structurii data nasterii:


struct persoana p1={"Popescu Ion", "Bucuresti, Magheru 14", {2, 4, 1960}};
printf("\n%i",p1.datan.an);

//Initializarea articolului din exemplul 2:


struct magazin gigel_srl={200, 1,2,3,4,5,6,7,8,9,10,11,12};

//sau cu evidentierea structurii de masiv:


struct magazin gigel_srl1={200, {1,2,3,4,5,6,7,8,9,10,11,12}};
printf("\n%6.2f",gigel_srl1.vanzari_lunare[10]);

//Initializarea articolului din exemplul 3 (doar primele 4 materii


//prime, restul de 26 vor fi initializate automat cu valori nule:
struct produs
z={243,5,{{2420,25.4},{3251,70.21},{1421,8.4},{51,7.2}}};
printf("\n%6.2f",z.materii_prime[2].norma); }

4.3 Articole cu structuri complexe


În activitatea de programare pot fi întâlnite aplicaţii care reclamă utilizarea
articolelor cu structură variabilă. La iniţializarea câmpurilor unui astfel de articol,
constanta de tip articol se asociază unei singure structuri, deoarece zona de
memorie rezervată pentru articol este unică. Pentru acest tip de articol, limbajul
pune la dispoziţia utilizatorilor tipul predefinit reuniune (union), care se comportă
ca şi tipul struct cu o singură diferenţă: la un moment dat al execuţiei programului,
în zona de memorie rezervată articolului nu este memorat decât unul dintre
câmpurile acestuia.

49
Programarea calculatoarelor

Declararea tipului reuniune se realizează astfel:


union nume_tip { tip_cimp1 cimp1;
tip_cimp2 cimp2;
................
tip_cimpn cimpn;};

Lungimea zonei de memorie rezervate pentru o variabilă de tip reuniune va


fi egală cu maximul dintre lungimile câmpurilor componente. Gestiunea
conţinutului respectivei zone de memorie va trebui realizată de către programator.
Exemplu:
Se presupune un articol cu structura din figura 4.4.
Forma de învăţământ
Data An de
Nume zi id
naşterii studiu
bursa valoare loc de muncă data angajării
lun
char[40] zi an int char float char[30] zi lună an
a

Figura 4.4 Articol cu structură variabilă

Declararea şi iniţializarea câmpurilor unui student la zi pentru structura articolului


din figura 6.4 se realizează astfel:
#include <stdio.h>
void main()
{//Declararea articolului cu structura variabila:
struct articol
{ char nume[40];
struct { int zi, luna, an;} datan;
int an_st;
char forma_inv;
union
{ struct {char bursa; float valoare;}zi;
struct {char loc_m[30];
struct {int zi, luna, an;}data_ang;
}id;
} parte_vb;
};
//Initializarea campurilor unui student la zi:
struct articol a={"Popescu Felix",{4,1,1974} ,1,'Z',{'D',250.5}};
printf("\nData nasterii: %i.%i.%i, Forma de inv.: %c, Val. bursa:
%6.2f", a.datan.zi, a.datan.luna, a.datan.an, a.forma_inv,
a.parte_vb.zi.valoare);
}
Din punct de vedere fizic, existenţa părţii variabile într-un articol
generează, la compilare, deplasări egale faţă de începutul articolului pentru toate
variantele de descriere. Astfel, pentru descrierea din exemplul de mai sus se
generează deplasarea 49 faţă de începutul articolului, atât pentru câmpul bursa, cât
şi pentru loc_m.

50
Tipul de dată articol

4.4 Constante de tip articol


Constantele de tip articol pot fi constante cu tip (variabile iniţializate la
compilare) şi constante „obiect”, pentru care în momentul compilării se rezervă
zone de memorie, iar câmpurile articolelor sunt iniţializate cu valorile precizate de
utilizator.
Valoarea iniţială trebuie să fie de acelaşi tip cu câmpul căruia îi
corespunde. Când articolul conţine la rândul său alt articol, identificarea câmpului
care se iniţializează se face pe niveluri, folosind perechi corespunzătoare de
acolade.
Constantele cu tip joacă rol de variabile care se iniţializează cu o valoare
în faza de compilare, ele putând să-şi modifice valoarea pe parcursul execuţiei
programului.
tip nume_const = {lista_valori};

Constantele obiect sunt variabile iniţializate la declarare, pentru care se


rezervă memorie, dar conţinutul lor nu poate fi modificat pe parcursul programului.

const tip nume_const = {lista_valori};

Exemplul 1:
#include<stdio.h>
void main()
{ struct persoana
{ char nume[40];
char adresa[30];
struct
{ int zi, luna, an;} datan;
};
persoana pers={"Popescu Ion", "Bucuresti; Magheru 14",
{2, 4, 1960}};
//constanta cu tip
pers.datan.zi=4;
}

Exemplul 2:
#include<stdio.h>
void main()
{ struct persoana
{ char nume[40];
char adresa[30];
struct {int zi, luna, an;} datan;
};
const persoana pers={"Popescu Ion", "Bucuresti; Magheru 14",
{2, 4, 1960}};
//constanta obiect
// pers.datan.zi=4; genereaza eroare la compilare
}

51
5 Fişiere de date

Prelucrarea automată a datelor presupune un sistem de organizare a


acestora după metode şi procedee specifice. Organizarea datelor este un proces
complex care include identificarea, clasificarea şi descrierea proprietăţilor acestora,
gruparea lor în colecţii, reprezentarea pe purtători tehnici, definirea şi realizarea
procedurilor de prelucrare etc.
Deoarece datele se memorează, de obicei, pe purtători tehnici de
informaţii, dar se prelucrează numai când sunt prezente în memoria internă, acestea
trebuie organizate atât extern cât şi intern. În organizarea externă a datelor se
identifică două niveluri de abordare, după cum se are în vedere acest proces din
perspectiva utilizatorului sau a purtătorilor fizici externi pe care se înregistrează
datele. Cele două niveluri de abordare, numite logic, respectiv fizic, precum şi
realizarea trecerii de la unul la celălalt, în condiţiile specifice diverselor sisteme de
calcul, se bazează pe o serie de concepte, cum ar fi: fişierul şi articolul, purtătorul
tehnic de date, metoda de organizare şi modul de acces, operaţiile de prelucrare etc.

5.1 Fişierul şi articolul

Fişierul reprezintă termenul generic care desemnează structurile de date


externe. El este o mulţime (colecţie) de date omogene din punct de vedere al
semnificaţiei şi al cerinţelor de prelucrare. În purtătorul extern, fişierul are, pe
lângă partea de date, şi alte informaţii de identificare (etichete).
Privit din punctul de vedere al prelucrării, un fişier este o colecţie ordonată
de date, numite articole. Articolul este constituit dintr-o mulţime ordonată de valori
ale unor caracteristici ce aparţin, uzual, unei singure entităţi (obiect, fenomen,
proces etc.) din domeniul de activitate abordat. De exemplu, într-un fişier care
conţine datele personale ale salariaţilor dintr-o unitate economică, un articol
grupează valorile caracteristicilor unei singure persoane.

52
Fişiere de date

Componentele articolului destinate diverselor caracteristici sunt denumite


câmpuri de date. Depinzând de natura, ordinul de mărime şi forma de reprezentare
externă a valorilor asociate, fiecare câmp de date are o lungime, exprimată uzual în
octeţi. Lungimea unui articol este dată de suma lungimilor câmpurilor care îl
compun. După cum toate articolele dintr-un fişier au sau nu aceeaşi lungime, se
face distincţie între fişierele cu articole de lungime fixă sau variabilă. Modul de
implementare fizică a celor două tipuri de fişiere diferă de la un sistem la altul şi
chiar de la un limbaj la altul.
Pe purtătorul fizic extern, partea de date a fişierului se prezintă ca o
succesiune de octeţi cu un conţinut binar fără semnificaţie informaţională. În
momentul prelucrării, prin descrieri şi operaţii adecvate, din succesiunea memorată
extern se „decupează" entităţi (articole, blocuri, linii sau câmpuri) cu structuri
corespunzătoare prelucrării. Tipul entităţii care se „decupează" depinde de tipul
fişierului.

5.2 Metode de organizare şi tipuri de acces


Principiile şi regulile după care se memorează articolele unui fişier pe
purtătorul extern, cu asigurarea protecţiei şi regăsirii acestora, constituie metoda de
organizare. În evoluţia organizării datelor externe s-au cristalizat mai multe
metode, dintre care, cele mai uzuale sunt secvenţială, relativă şi indexată.
Principala diferenţiere între metodele de organizare o reprezintă tipurile de acces
admise.
Tipul de acces reprezintă modalitatea de regăsire (localizare) a articolelor
din fişier. Noţiunea de acces trebuie aplicată atât pentru operaţia de scriere, cît şi
pentru cea de citire a datelor.
Poziţia din/în care se face citirea/scrierea în cadrul fişierului este indicată
de un pointer. Accesul la datele înregistrate pe un purtător tehnic poate fi secvenţial
sau direct, în funcţie de modul în care se stabileşte pointerul.
Accesul secvenţial este posibil la toţi purtătorii tehnici de date şi
presupune înscrierea înregistrărilor în ordinea furnizării lor sau regăsirea în ordinea
în care au fost înscrise în suport (figura 5.1).
P(Ak)=f (P(Ak-1))
Traversare

A1 A2 ... Ak-1 Ak ... An EOF

Figura 5.1 Principiul de realizare a accesului secvenţial la articole


Pointerul de fişier avansează, în scriere şi citire, de la o entitate (articol,
bloc, linie sau câmp) la alta. Dacă pointerul se exprimă prin deplasare faţă

53
Programarea calculatoarelor

de începutul fişierului, atunci, matematic, acest lucru se poate exprima astfel:


P(A1) = 0;
P(Ak) = f(P(Ak-1)) = P(Ak-1)+lartk-1; pentru k=2,n;
unde Ak este articolul k şi lartk este lungimea articolului k.

O problemă importantă care se pune la consultarea (citirea) în acces


secvenţial este controlul ajungerii la sfârşitul fişierului. După citirea ultimei entităţi
(articol, bloc, linie sau câmp), pointerul indică marcatorul de sfârşit de fişier -
EOF (figura 5.2).
Poziţia pointerului după
citirea ultimului articol

A1 A2 ... Ak-1 Ak ... An EOF

Figura 5.2 Pointerul după citirea ultimului articol din fişier


În limbajele de programare se regăsesc două modalităţi de sesizare a
sfârşitului de fişier:
a) Sesizarea sfârşitului de fişier în cadrul operaţiei de citire (limbajele
FORTRAN, COBOL, C). Sfârşitul este sesizat la citirea marcatorului de sfârşit de
fişier. Situaţia din figura 5.2 nu este considerată sfârşit de fişier. Abia la următoarea
citire se întâmplă acest lucru (pointerul de fişier avansează după marcatorul de
sfârşit de fişier).
b) Sesizarea sfârşitului de fişier independent de operaţia de citire
(limbajele BASIC, PASCAL). În acest caz, dacă pointerul este pe marcatorul de
sfârşit de fişier (după ultimul articol, bloc, linie, câmp, ca în figura 5.2) se
consideră sfârşit de fişier. Următoarea citire produce eroare de intrare/ieşire (I/E).
Proiectarea algoritmilor de prelucrare a fişierelor este determinată de
modalitatea în care se sesizează sfârşitul de fişier.
Accesul direct este posibil numai la fişierele care au o anumită organizare,
au ca entitate de transfer articolul sau blocul şi sunt memorate pe discuri
magnetice. Accesul direct se bazează pe existenţa unui algoritm implementat în
sistem care asigură regăsirea (localizarea) articolelor în funcţie de o informaţie de
regăsire. Valoarea pointerului este determinată direct, fără să depindă de valoarea
sa anterioară: P(Ak)=f(irk), unde Ak este articolul k, iar irk este o informaţie de
regăsire a articolului k. În funcţie de algoritmul şi informaţia de regăsire, există
două tipuri de acces direct: după cheie şi după numărul relativ al articolului.
În cazul accesului direct după cheie, articolul este regăsit prin aplicarea
unui algoritm asupra unei informaţii de identificare de tip cheie: P(Ak)=f(cheiek).
În cazul accesului direct după numărul relativ - care se mai numeşte, simplu, acces
relativ - (figura 5.3), articolul este localizat în fişier prin numărul său, stabilit, în
cadrul fişierului, de la valoarea zero: P*(Ak)=(k-1); P(Ak)=P*(Ak)×lart. P*(Ak)
reprezintă poziţia exprimată în număr relativ, iar P(Ak) reprezintă poziţia exprimată

54
Fişiere de date

prin deplasare, în octeţi, faţă de începutul fişierului (la unele sisteme numărul
relativ este stabilit de la unu: P*(Ak)=k).
La scriere, articolul Ak (numărul relativ k-1) se memorează pe poziţia sa,
celelalte k-1 articole anterioare putând să nu existe (pe suport există însă rezervat
loc pentru ele). La citire, articolul Ak (cu numărul relativ k-1, kn) este localizat
direct şi conţinutul lui se transferă în memoria internă.
Acces direct prin numărul relativ k-1
P(Ak)=k-1

A1 A2 ... Ak-1 Ak ... An EOF

0 1 ... k k-1 ... n-1


Număr relativ
Figura 5.3 Principiul de realizare a accesului direct prin număr relativ

Fişierele organizate secvenţial, cu articole de lungime variabilă, admit


numai accesul secvenţial. Fişierele organizate secvenţial, cu articole sau blocuri de
lungime fixă, admit atât accesul secvenţial, cât şi pe cel relativ. Acest lucru derivă
din faptul că accesul relativ este realizat de sistem printr-o deplasare secvenţială
faţă de începutul acestuia, deplasare care este egală cu valoarea expresiei:
număr_relativ × lungime_articol.

5.3 Structura sistemului de fişiere sub MS-DOS/Windows


Sistemul de operare MS-DOS utilizează o formă logică arborescentă de
grupare a fişierelor de pe discuri în directoare şi subdirectoare. Un director
(subdirector) poate conţine fişiere şi/sau alte subdirectoare (figura 5.4). În limbajul
curent folosit de practicieni se utilizează noţiunea de director şi în cazul
subdirectoarelor.
Un disc DOS conţine un singur director rădăcină, care la rândul lui are zero
sau mai multe subdirectoare şi/sau fişiere. Subdirectoarele pot avea oricâte niveluri
de imbricare. Frunzele arborelui sunt, cel mai adesea, fişiere, dar pot fi şi
subdirectoare vide. Unitatea de disc, directorul şi subdirectorul în care se lucrează
la un moment dat se numesc curente.
Pentru ca un fişier să fie localizat în cadrul structurii arborescente se
foloseşte un identificator extern (specificator) care are următoarea formă sintactică:
[n:][cale][\]nume_fişier[.extensie]
n este numele unităţii de disc (A:, B:, C: etc.). Prin lipsă, se consideră
unitatea curentă;
cale (path) este calea de acces de la directorul rădăcină până la
subdirectorul dorit. Fiecare nume de director (subdirector) din interiorul căii este

55
Programarea calculatoarelor

precedat de caracterul backslash (\). Prin lipsă, se consideră calea subdirectorului


curent. Calea selectată la un moment dat poate începe de la rădăcină sau de la
subdirectorul curent. Când calea începe cu caracterul backslash (\) căutarea începe
de la rădăcină; în caz contrar, căutarea începe de la directorul curent.

Rădăcina (pe discul C:\)

F1 D1 D2 D3 F2 F3

F4 F2 D4 D5 F5 F6 F7

F8 F9

Figura 5.4 Exemplu de structură arborescentă de directori


Fiecare subdirector conţine două intrări speciale marcate prin caracterul
".", respectiv caracterele ".." în locul numelui de fişier. Prima intrare realizează
„autopunctarea”, indicând faptul că entitatea este subdirector (nu fişier de date), a
doua intrare „punctează” subdirectorul părinte. În construirea căii de acces se poate
folosi succesiunea de două puncte pentru a indica subdirectorul părinte.
nume_fişier este numele extern al fişierului, format din maxim 8 caractere
alfanumerice, mai puţin unele caractere speciale, ca: ." \ / : ' > < + = ; , ). Există o
serie de nume prestabilite, asociate unor dispozitive standard de intrare/ieşire, care
nu pot fi utilizate de programator pentru propriile fişiere: CON, AUX, COM1,
COM2, LPT1, LPT2, LPT3, NULL, PRN, CLOCK$ (dispozitiv pentru ceasul de
timp real).
extensia este formată din maxim trei caractere alfanumerice prin care
utilizatorul are posibilitatea să-şi identifice fişiere cu conţinuturi diferite. Prin lipsă
nu se asumă nicio valoare.

Exemple (structura din figura 5.4):


1. C:\F1 → Fişierul F1 din rădăcină;
2. C:\D2\F2 → Fişierul F2 din subarborele C:-D2;
3. C:\F2 → Fişierul F2 din directorul rădăcină;
4. C:\D2\D4\F9 → Fişierul F9 din subarborele C:-D2-D4;
5. Pentru a indica fişierul F9 se poate folosi una din scrierile:
C:\D2\D4\F9 → de oriunde;
\D2\D4\F9 → de oriunde din unitatea C:;
..\D4\F9 → din subdirectorul D5;
F9 → din subdirectorul D4.

56
Fişiere de date

Producătorii de software au impus unele denumiri de extensii, care, deşi


opţionale, oferă posibilitatea simplificării referirii fişierelor în unele comenzi sau
aplicaţii.

Exemple:
6. Standard MS-DOS:
.COM → program executabil;
.EXE → program executabil;
.SYS → driver de sistem;
.OBJ → program obiect;
.BAT → fişiere de comenzi DOS (prelucrări BATCH).
7. Standarde de firmă:
.ARC → arhivă compactată cu PKPAK sau ARC;
.ZIP → arhivă compactată cu PKZIP sau WINZIP;
.DBF → bază de date DBASE.
8. Formate ASCII:
.ASM → program sursă ASSEMBLER;
.BAS → program sursă BASIC;
.PAS → program sursă PASCAL;
.CBL → program sursă COBOL;
.C → program sursă C;
.TXT → fişier text;
9. Formate grafice:
.PCX → Paint Brush;
.MSP → Microsoft Windows;
.WPG → WordPerfect.

5.4 Operaţii de prelucrare a fişierelor


Asupra unui fişier se pot executa diverse operaţii de prelucrare, numite şi
de gestiune, care se împart în operaţii la nivel de fişier şi la nivel de articol.

Operaţiile la nivel de fişier se referă la aspecte ca: înscrierea fişierului în


[sub]directoare, validarea şi interzicerea accesului la fişier (deschidere/închidere),
ştergerea fişierului din [sub]directoare (ştergere) etc. Aceste operaţii se regăsesc, în
totalitate, la prelucrarea fişierelor pe discuri magnetice. În cazul purtătorilor
nereutilizabili, singurele operaţii care au sens sunt cele de deschidere/închidere a
fişierelor.
Operaţiile la nivel de articol se referă la accesul la entităţile de date ale
fişierului (articole, blocuri, linii sau câmpuri) în vederea prelucrării lor. Privite sub
aspectul semnificaţiei pentru utilizator, aceste operaţii se referă la: înscrierea
iniţială a entităţilor pe purtătorul tehnic (populare), actualizarea fişierului prin
includerea de noi entităţi (adăugare), modificarea valorilor unor câmpuri din

57
Programarea calculatoarelor

anumite entităţi (modificare), eliminarea entităţilor care nu mai sunt necesare


(ştergere), regăsirea entităţilor în vederea satisfacerii unor cerinţe de informare
(consultare). În programele C, operaţiile de I/E sunt realizate cu ajutorul unei
mulţimi de funcţii specializate pentru căutare, scriere, citire etc.
În concluzie, dacă din punctul de vedere al utilizatorului operaţiile de
prelucrare se descriu relativ simplu, prin apeluri de funcţii, realizarea efectivă a lor
de către sistemul de calcul este complexă. În sistemul de operare MS-DOS sunt
incluse funcţii de întrerupere care, prin intermediul BIOS (Basic Input Output
System), lansează anumite operaţii cu un echipament.
Din punct de vedere al reprezentării datelor în suportul extern, se disting
fişiere text, în care toate datele sunt sub formă ASCII (un caracter/octet) şi fişiere
binare, în care toate datele sunt memorate în forma identică cu cea din memoria
principală (MP).
Strâns legat de lucrul cu cele două tipuri de fişiere este modul în care se
face transferul datelor între memoria principală şi suportul extern: transfer posibil
cu conversie (în cazul fişierelor text) şi transfer fără conversie (în cazul fişierelor
binare).
Trebuie făcută remarca, deosebit de importantă, că din punct de vedere
fizic fişierul se reprezintă în suportul extern ca o succesiune de octeţi. Această
succesiune poate fi tratată logic ca un fişier de un tip sau altul. Este sarcina
programatorului să asigure „suprapunerea” corectă a fişierului logic peste cel fizic.
Din acest punct de vedere se poate spune că prin fişier logic se înţelege, prioritar,
un mod de prelucrare şi mai puţin un mod de memorare.
Indiferent de limbajul de programare folosit, operaţiile necesare pentru
prelucrarea fişierelor sunt:
• descrierea fişierului (crearea tabelei care memorează caracteristicile
fişierului);
• asignarea fişierului intern (numele logic) la unul extern (fizic);
• deschiderea fişierului;
• operaţii de acces la date („articole”);
• închiderea fişierului.
Pentru lucrul cu fişiere trebuie identificate tipurile acestora, metodele de
organizare, modurile de acces şi tipurile de articole acceptate. Din punct de vedere
al tipurilor de date, în C există un singur tip de fişiere: flux de octeţi (înşiruire de
octeţi, fără niciun fel de organizare sau semnificaţie). Organizarea acestui flux de
octeţi este secvenţială. Accesul la fişiere se poate face secvenţial sau direct (cu
excepţia fişierelor standard, la care accesul este numai secvenţial). În bibliotecile
limbajului există funcţii predefinite pentru prelucrarea fişierelor. Funcţiile de
prelucrare la nivel superior a fişierelor tratează fluxul de octeţi acordându-i o
semnificaţie oarecare. Putem spune că din punct de vedere al prelucrării, la acest
nivel, ne putem referi la fişiere text şi fişiere binare.
Există fişiere standard, care sunt gestionate automat de sistem, dar asupra
cărora se poate interveni şi în mod explicit. Acestea sunt:
• fişierul standard de intrare (stdin);

58
Fişiere de date

• fişierul standard de ieşire (stdout);


• fierul standard pentru scrierea mesajelor de eroare (stderr);
• fişierul standard asociat portului serial (stdaux);
• fişierul standard asociat imprimantei cuplate la portul paralel (stdprn).
Fişierele standard pot fi redirectate conform convenţiilor sistemului de
operare, cu excepţia lui stderr care va fi asociat întotdeauna monitorului.
În lucrul cu fişiere (sau la orice apel de sistem), în caz de eroare în timpul
unei operaţii se setează variabila errno, definită în errno.h, stddef.h şi stdlib.h.
Valorile posibile sunt definite în stdlib.h.
În limbajul C există două niveluri de abordare a lucrului cu fişiere: nivelul
inferior de prelucrare (fără gestiunea automată a zonelor tampon de intrare/ieşire)
şi nivelul superior de prelucrare (se folosesc funcţii specializate de gestiune a
fişierelor). În continuare, prin specificator de fişier se va înţelege un nume extern
de fişier, conform convenţiilor sistemului de operare. Specificatorul de fişier poate
să conţină strict numele fişierului sau poate conţine şi calea completă până la el.

5.4.1 Nivelul inferior de prelucrare a fişierelor

Nivelul inferior de prelucrare este folosit rar, numai în programele de


sistem. La acest nivel, descrierea fişierelor se realizează în corpul programelor,
caracteristicile acestora obţinându-se din context. Maniera de prelucrare este
asemănătoare celei de la nivelul sistemului de operare. Nu există un tip anume de
dată, fişierul fiind referit printr-un index care indică intrarea într-o tabelă de
gestiune a resurselor sistemului de operare. Acest index este de tip int şi se numeşte
manipulator de fişier (handle). Manipulatorul este creat şi gestionat de către
sistemul de operare. Utilizatorul îl foloseşte pentru a indica sistemului fişierul
asupra cărui doreşte să facă prelucrări.
Pentru utilizarea acestui nivel, în programul C trebuie incluse bibliotecile
standard io.h, stat.h şi fcntl.h.

Crearea şi asignarea unui fişier nou se realizează prin apelul funcţiei


creat, care are următorul prototip:

int creat(const char* numef, int protecţie);

Funcţia returnează manipulatorul fişierului nou creat; numef este un pointer


spre un şir de caractere care defineşte specificatorul de fişier, iar protecţie defineşte
modul de protecţie a fişierului creat (protecţia este dependentă de sistemul de
operare). În biblioteca stat.h sunt definite următoarele valori pentru parametrul
protecţie: S_IREAD (citire), S_IWRITE (scriere), S_IEXEC (execuţie). Aceste
valori pot fi combinate folosind operatorul | (sau logic pe biţi). Funcţia creat poate
fi apelată şi pentru un fişier existent. Efectul produs este ştergerea fişierului
existent şi crearea unuia gol, cu acelaşi nume; conţinutul fişierului existent se
pierde. În caz de eroare se returnează valoarea –1 şi se setează variabila globală

59
Programarea calculatoarelor

errno, care defineşte tipul erorii. Valorile obişnuite pentru errno sunt EBADF
(manipulator eronat, nu a fost găsit fişierul) sau EACCES (fişierul nu poate fi
accesat).
Deschiderea unui fişier existent se realizează prin apelul funcţiei open,
care are următorul prototip:

int open(const char *path,int access[,unsigned mod]);

Funcţia returnează manipulatorul fişierului; numef este pointer spre un şir


de caractere care defineşte specificatorul de fişier; acces este modul de acces la
fişier; constantele care descriu modurile de acces la fişier sunt descrise în fcntl.h.
Cele mai importante sunt: O_RDONLY – fişierul va fi accesat numai pentru citire;
O_WRONLY – fişierul va fi accesat numai pentru scriere; O_RDWR – fişierul va
fi accesat atât pentru citire, cât şi pentru scriere; O_CREAT: fişierul va fi creat ca
nou. Aceste moduri pot fi combinate folosind operatorul |. Mod este folosit numai
dacă parametrul acces conţine şi valoarea O_CREAT, caz în care indică modul de
protecţie a acestuia: S_IWRITE – se permite scrierea în fişier; S_IREAD – se
permite citirea din fişier; S_IREAD|S_IWRITE – se permite atât scrierea, cât şi
citirea din fişier.
Citirea dintr-un fişier se realizează prin apelul funcţiei read, care are
următorul antet:

int read(int nf, void* zonat, unsigned n);

Funcţia returnează numărul de octeţi citiţi din fişier; nf este manipulatorul


de fişier (alocat la crearea sau deschiderea fişierului), zonat este un pointer spre
zona tampon în care se face citirea (aceasta este definită de programator), iar n este
dimensiunea zonei receptoare (numărul maxim de octeţi care se citesc). Numărul
maxim de octeţi care pot fi citiţi este 65534 (deoarece 65535 – 0xFFF – se
reprezintă intern la fel ca -1, indicatorul de eroare). În cazul citirii sfârşitului de
fişier se va returna valoarea 0 (0 octeţi citiţi), iar la eroare se returnează -1 (tipul
erorii depinde de sistemul de operare). Fişierul standard de intrare (stdin) are
descriptorul de fişier 0.

Scrierea într-un fişier se realizează prin apelul funcţiei write, care are
următorul prototip:

int write(int nf, void* zonat, unsigned n);

Funcţia returnează numărul de octeţi scrişi în fişier; nf este manipulatorul


de fişier (alocat la crearea sau deschiderea fişierului), zonat este un pointer spre
zona tampon din care se face scrierea (aceasta este definită de programator); n este
numărul de octeţi care se scriu. Numărul maxim de octeţi care pot fi citiţi este
65534 (deoarece 65535 – 0xFFF – se reprezintă intern la fel ca -1, indicatorul

60
Fişiere de date

de eroare). În general, trebuie ca la revenirea din funcţia write, valoarea returnată


să fie egală cu n; dacă este mai mică, s-a produs o eroare (probabil discul este plin).
La scrierea în fişiere text, dacă în fluxul octeţilor care se scriu apare caracterul LF,
write va scrie în fişier perechea CR/LF. În caz de eroare, valoarea returnată este -1
şi se setează variabila errno. Fişierul standard de ieşire (stdout) are manipulatorul
1, iar cel de eroare (stderr) are manipulatorul 2.

Închiderea unui fişier se realizează prin apelul funcţiei close, care are
următorul prototip:

int close(int nf);

Funcţia returnează valoarea 0 (închidere cu succes) sau -1 (eroare); nf este


manipulatorul de fişier. De asemenea, închiderea unui fişier se realizează automat,
dacă programul se termină prin apelul funcţiei exit.

Poziţionarea într-un fişier se realizează prin apelul funcţiei lseek, care are
următorul prototip:

long lseek(int nf, long offset, int start);

Funcţia returnează poziţia faţă de începutul fişierului, în număr de octeţi; nf


este manipulatorul de fişier; offset este un parametru de tip long (numărul de octeţi
peste care se va deplasa pointerul în fişier), iar start este poziţia faţă de care se face
deplasarea: 0 (începutul fişierului), 1 (poziţia curentă în fişier) sau 2 (sfârşitul
fişierului). La eroare returnează valoarea -1L.

Exemple:
1. Apelul vb=lseek(nf, 0l, 2); Ù realizează poziţionarea la sfârşitul fişierului
(în continuare se poate scrie în fişier folosind write);

2. Apelul vb=lseek(nf, 0l, 0); Ù realizează poziţionarea la începutul


fişierului.

Ştergerea unui fişier existent se realizează prin apelul funcţiei unlink, care
are următorul prototip:

int unlink(const char* numef);

Funcţia returnează 0 (ştergere cu succes) sau -1 (eroare); numef este un


pointer spre un şir de caractere care defineşte specificatorul de fişier. În caz de
eroare se setează variabila errno cu valoarea ENOENT (fişierul nu a fost găsit) sau
EACCES (accesul interzis pentru această operaţie, de exemplu pentru fişiere read
only). Pentru a putea şterge un fişier read only trebuie mai întâi schimbate

61
Programarea calculatoarelor

drepturile de acces la fişier, folosind funcţia chmod:

int chmod(const char *cale, int mod);

unde cale este specificatorul de fişier, iar mod noile permisiuni. Permisiunile sunt
aceleaşi ca la funcţia open. Rezultatul întors de chmod are aceeaşi semnificaţie ca
şi unlink.

Verificarea atingerii sfârşitului de fişier se face folosind funcţia eof:

int eof(int nf);

unde nf este manipulatorul fişierului. Funcţia returnează valoarea 1 dacă pointerul


este poziţionat pe sfârşitul fişierului, 0 în caz contrat şi -1 în caz de eroare (nu este
găsit fişierul – errno primeşte valoarea EBADF).

Exemplu:

#include <sys\stat.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>

int main(void)
{ int handle;
char msg[] = "This is a test";
char ch;
/* create a file */
handle = open("TEST.$$$", O_CREAT | O_RDWR, S_IREAD | S_IWRITE);
/* write some data to the file */
write(handle, msg, strlen(msg));
/* seek to the begining of the file */
lseek(handle, 0L, SEEK_SET);
/* reads chars from the file until we hit EOF */
do {read(handle, &ch, 1);
printf("%c", ch);}
while (!eof(handle));
close(handle);
return 0;}

Bibliotecile limbajului conţin şi alte funcţii pentru prelucrarea fişierelor la


nivel inferior, inclusiv variante ale funcţiilor anterioare, apărute odată cu
dezvoltarea sistemelor de operare.

62
Fişiere de date

5.4.2 Nivelul superior de prelucrare a fişierelor

La acest nivel, un fişier se descrie ca pointer către o structură predefinită


(FILE – tabela de descriere a fişierului (FIB)):
FILE* f;

Tipul FILE (descris în stdio.h) depinde de sistemul de operare.


Fişierul este considerat ca flux de octeţi, din care funcţiile de prelucrare
preiau secvenţe pe care le tratează într-un anumit fel (sau în care inserează secvenţe
de octeţi).
Funcţiile folosite la acest nivel pot fi împărţite în trei categorii: funcţii de
prelucrare generale, funcţii de citire/scriere cu conversie şi funcţii de citire/scriere
fără conversie. Funcţiile de prelucrare generală se aplică tuturor fişierelor,
indiferent de tipul informaţiei conţinute; prelucrarea efectuată de acestea nu are
niciun efect asupra conţinutului fişierului. Funcţiile care lucrează cu conversie se
aplică fişierelor care conţin informaţie de tip text (linii de text, separate prin
perechea CR/LF, iar la sfârşit se găseşte caracterul CTRL-Z). Funcţiile care
lucrează fără conversie se aplică fişierelor care conţin informaţie binară.
Funcţiile de citire/scriere deplasează pointerul de citire/scriere al fişierului,
spre sfârşitul acestuia, cu un număr de octeţi egal cu numărul de octeţi transferaţi
(fără a trece de sfârşitul de fişier).

Funcţii de prelucrare generală

Deschiderea şi asignarea se realizează prin apelul funcţiei fopen. Funcţia


returnează un pointer spre o structură de tip FILE (în care sunt înscrise date
referitoare la fişierul deschis) sau NULL dacă fişierul nu se poate deschide:

FILE* fopen(const char* nume_extern,const char* mod);

Parametrul nume_extern constituie specificatorul de fişier iar mod este un şir de


caractere care specifică modul de deschidere a fişierului. Asignarea se realizează
prin expresie de atribuire de tipul:

nume_intern=fopen(sir_nume_extern,sir_mod);

Exemplu:
FILE* f;
f = fopen("PROD.DAT","r");

63
Programarea calculatoarelor

Modurile în care poate fi deschis un fişier sunt prezentate în tabelul 5.1.

Modurile de deschidere a unui fişier


Tabelul 5.1
Mod Scop
Deschide un fişier existent pentru adăugare la sfârşit (extindere) sau îl creează
a
dacă nu există. Este permisă numai scrierea. Numai pentru fişiere text.
r Deschide un fişier existent numai pentru citire.
Suprascrie un fişier existent sau creează unul nou, permiţându-se numai
w
operaţia de scriere.
Deschide un fişier existent pentru adăugare la sfârşit (extindere) sau îl creează
a+
dacă nu există. Sunt permise citiri şi scrieri. Numai pentru fişiere text.
r+ Deschide un fişier existent pentru citire şi scriere
Suprascrie un fişier existent sau creează unul nou, permiţându-se atât citiri,
w+
cât şi scrieri.

La opţiunile de mai sus se poate adăuga b pentru fişiere binare sau t pentru
fişiere text. Dacă nu este prezentă nici litera b, nici litera t, modul considerat
depinde de valoarea variabilei _fmode: dacă valoarea este O_BINARY, se
consideră fişier binar; dacă valoarea este O_TEXT, se consideră fişier text. De
obicei implicită este valoarea O_TEXT.
Modurile uzuale pentru deschiderea fişierelor sunt prezentate în tabelul 5.2.

Moduri uzuale pentru deschiderea fişierelor


Tabelul 5.2
Operaţia de gestiune Fişiere text Fişiere binare
Creare w wb
Consultare r rb
Actualizare r+b
Creare şi actualizare w+ rwb, w+b
Extindere a

Închiderea fişierelor se realizează prin apelul funcţiei fclose, care are


următorul prototip:

int fclose(FILE* f);

Funcţia închide fişierul primit ca parametru şi returnează valoarea 0 în caz


de succes sau -1, în caz de eroare. Înainte de închiderea fişierului, sunt golite toate
zonele tampon asociate lui. Zonele tampon alocate automat de sistem sunt
eliberate.

64
Fişiere de date

Revenirea la începutul fişierului se realizează prin funcţia rewind, cu


prototipul:

void rewind(FILE *f);

Executarea funcţiei are ca efect poziţionarea la începutul fişierului f (care


era deschis anterior), resetarea indicatorului de sfârşit de fişier şi a indicatorilor de
eroare (se înscrie valoarea 0). După apelul lui rewind poate urma o operaţie de
scriere sau citire din fişier.

Testarea sfîrşitului de fişier, se realizează prin apelul macrodefiniţiei feof:

int feof(FILE* f);

Macro-ul furnizează valoarea indicatorului de sfârşit de fişier asociat lui f.


Valoarea acestui indicator este setată la fiecare operaţie de citire din fişierul
respectiv. Valoarea întoarsă este 0 (fals) dacă indicatorul are valoarea sfârşit de
fişier şi diferit de zero (adevărat) în caz contrar. Apelul lui feof trebuie să fie
precedat de apelul unei funcţii de citire din fişier. După atingerea sfârşitului de
fişier, toate încercările de citire vor eşua, până la apelul funcţiei rewind sau
închiderea şi redeschiderea fişierului.
Golirea explicită a zonei tampon a unui fişier se realizează prin apelul
funcţiei fflush, care are următorul prototip:

int fflush(FILE* f);

Dacă fişierul f are asociată o zonă tampon de ieşire, funcţia scrie în fişier
toate informaţiile din acesta, la poziţia curentă. Dacă fişierul are asociată o zonă
tampon de intrare, funcţia îl goleşte. În caz de succes returnează valoarea zero, iar
în caz de eroare valoarea EOF (definită în stdio.h).

Exemplu:
Înainte de a citi un şir de caractere de la tastatură, zona tampon trebuie golită
pentru a preveni citirea unui şir vid (datorită unei perechi CR/LF rămase în zona
tampon de la o citire anterioară a unei valori numerice). Ştergerea se realizează prin
apelul:

fflush(stdin);

Aflarea poziţiei curente în fişier se realizează prin apelul uneia din funcţiile
fgetpos sau ftell:

int fgetpos(FILE* f,fpos_t* poziţie);

65
Programarea calculatoarelor

După apel, la adresa poziţie se află poziţia pointerului de citire/scriere din fişierul f,
ca număr relativ al octetului curent. Primul octet are numărul 0. Valoarea returnată
poate fi folosită pentru poziţionare cu funcţia fsetpos. În caz de succes funcţia
întoarce valoarea 0, iar în caz de eroare o valoare nenulă şi setează variabila errno
la valoarea EBADF sau EINVAL.

long ftell(FILE* f);

returnează poziţia în fişierul f a pointerului de citire/scriere în caz de succes sau -1L


în caz contrar. Dacă fişierul este binar, poziţia este dată în număr de octeţi faţă de
începutul fişierului. Valoarea poate fi folosită pentru poziţionare cu funcţia fseek.

Modificarea poziţiei pointerului de citire/scriere se poate face prin


poziţionare relativă:

int fseek(FILE* f,long deplasare,int origine);

unde deplasare reprezintă numărul de octeţi cu care se deplasează pointerul în


fişierul f, iar origine reprezintă poziţia faţă de care se deplasează pointerul.
Parametrul origine poate fi: SEEK_SET (0) – poziţionare faţă de începutul
fişierului; SEEK_CUR (1) – poziţionare faţă de poziţia curentă; SEEK_END (2) –
poziţionare faţă de sfârşitul fişierului. Funcţia returnează valoarea 0 în caz de
succes (şi uneori şi în caz de eşec). Se semnalează eroare prin returnarea unei
valori nenule numai în cazul în care f nu este deschis.
Poziţionarea absolută se face cu funcţia:

int fsetpos(FILE* f,const fpos_t poziţie);

Pointerul de citire/scriere se mută în fişierul f la octetul cu numărul indicat


de parametrul poziţie (care poate fi o valoare obţinută prin apelul lui fgetpos).
Ambele funcţii resetează indicatorul de sfârşit de fişier şi anulează efectele
unor eventuale apeluri anterioare ale lui ungetc asupra acelui fişier.

Redenumirea sau mutarea unui fişier existent se poate realiza prin apelul
funcţiei rename, care are următorul prototip:
int rename(const char* n_vechi,const char* n_nou);

unde n_vechi reprezintă vechiul nume al fişierului, iar n_nou reprezintă numele
nou.
Dacă numele vechi conţine numele discului (de exemplu C:), numele nou
trebuie să conţină acelaşi nume de disc. Dacă numele vechi conţine o cale, numele
nou nu este obligat să conţină aceeaşi cale. Folosind o altă cale se obţine mutarea
fişierului pe disc. Folosind aceeaşi cale (sau nefolosind calea) se obţine
redenumirea fişierului. Nu sunt permise wildcard-uri (?, *) în cele două nume.

66
Fişiere de date

În caz de succes se întoarce valoarea 0. În caz de eroare se întoarce -1 şi


errno primeşte una din valorile: ENOENT – nu există fişierul, EACCES – nu
există permisiunea pentru operaţie sau ENOTSAM – dispozitiv diferit (mutarea se
poate face doar pe acelaşi dispozitiv).

Ştergerea unui fişier existent se poate realiza prin apelul funcţiei unlink,
prezentată anterior, sau remove, care are următorul prototip:

int remove(const char* cale);

unde cale reprezintă specificatorul fişierului (trebuie să fie închis).

Funcţii de citire/scriere fără conversie

Funcţiile efectuează transferuri de secvenţe de octeţi între memoria internă


şi un fişier de pe disc, fără a interveni asupra conţinutului sau ordinii octeţilor
respectivi.

Citirea dintr-un fişier binar se realizează prin apelul funcţiei fread, care are
următorul prototip:

size_t fread(void* ptr,size_t dim,size_t n,FILE* f);

Funcţia citeşte din fişierul f, de la poziţia curentă, un număr de n entităţi,


fiecare de dimensiune dim, şi le depune, în ordinea citirii, la adresa ptr. fread
returnează numărul de entităţi citite. În total se citesc, în caz de succes, n*dim
octeţi. În caz de eroare sau când se întâlneşte sfârşitul de fişier, funcţia returnează o
valoare negativă sau 0; size_t este definit în mai multe header-e (între care stdio.h)
şi este un tip de dată folosit pentru a exprima dimensiunea obiectelor din memorie.
Este compatibil cu tipul unsigned.

Exemplu:

struct complex {int x,y} articol;


FILE * f_complex;
if(f_complex=fopen("NR_COMPL.DAT", "rb")
fread(&articol,sizeof(articol),1,f_complex);
else printf("Fisierul nu poate fi deschis");

În exemplul anterior se deschide un fişier binar din care se citeşte un


articol de tip struct complex, care se depune în variabila articol.
Scrierea într-un fişier binar se poate realiza prin apelul funcţiei fwrite, care
are următorul prototip:
size_t fwrite(const void* ptr,size_t dim,size_t n,FILE*
f);

67
Programarea calculatoarelor

Funcţia scrie în fişierul f, începând cu poziţia curentă, un număr de n


entităţi contigue, fiecare de dimensiune dim, aflate în memorie la adresa ptr; fwrite
returnează numărul entităţilor scrise cu succes. În caz de eroare se returnează o
valoare negativă.

Exemplu:

struct complex {int x,y} articol;


FILE *pf;
pf=fopen("NR_COMPL.DAT","wb");
fwrite(& articol,sizeof (articol),1,pf);

Exemplul anterior creează un fişier binar nou în care scrie o secvenţă de


octeţi conţinând reprezentarea binară a unei date de tip struct complex.

Exemplu:
Să se scrie funcţia care calculează numărul de articole dintr-un fişier binar,
cunoscând lungimea în octeţi a unui articol. Funcţia are ca parametri fişierul şi
lungimea în octeţi a unui articol. Prin numele funcţiei se întoarce numărul de
articole din fişier.
int nrart(FILE *f, int l)
{ long p;
int n;
p=ftell(f);
fseek(f,0,2);
n=ftell(f)/l;
fseek(f,0,p);
return n;}

Funcţii de citire/scriere cu conversie

Funcţiile efectuează transferuri de secvenţe de octeţi între memoria internă


şi un fişier de pe disc, convertind secvenţa de la reprezentarea internă (binară) la
reprezentarea externă (ASCII) şi invers.
Transferul de caractere se efectuează prin următoarele funcţii:

int fgetc(FILE* f);


int fputc(int c, FILE *f);
int getc(FILE* f);
int putc(int c, FILE *stream);

Funcţia fgetc şi macrodefiniţia getc returnează următorul caracter din


fişierul f (după ce îl converteşte la reprezentarea de tip întreg fără semn). Dacă s-a
ajuns la sfîrşitul fişierului, funcţia va întoarce EOF (valoarea -1). Tot EOF va
întoarce şi dacă sunt probleme la citirea din fişier.
Funcţia fputc şi macrodefiniţia putc scriu caracterul c în fişierul f. În caz de
eroare se returnează valoarea c, altfel se returnează EOF.

68
Fişiere de date

Funcţia ungetc pune caracterul c în zona tampon de citire asociată


fişierului f. La următoarea citire cu fread sau getc acesta va fi primul octet/caracter
citit. Un al doilea apel al funcţiei ungetc, fără să fie citit primul caracter pus în flux,
îl va înlocui pe acesta. Apelarea funcţiilor fflush, fseek, fsetpos, sau rewind şterge
aceste caractere din flux. În caz de succes, ungetc returnează caracterul c, iar în caz
de eroare returnează EOF.

Transferul de şiruri de caractere se efectuează prin funcţiile:

char* fgets(char* s,int n,FILE* f);


int fputs(const char* s,FILE* f);

Funcţia fgets citeşte un şir de caractere din fişierul f şi îl depune la adresa s.


Transferul se încheie atunci când s-au citit n-1 caractere sau s-a întâlnit caracterul
newline. La terminarea transferului, se adaugă la sfârşitul şirului din memorie
caracterul nul ‘\0’. Dacă citirea s-a terminat prin întâlnirea caracterului newline,
acesta va fi transferat în memorie, caracterul nul fiind adăugat după el (spre
deosebire de gets, care nu îl reţine). La întâlnirea sfârşitului de fişier (fără a fi
transferat vreun caracter) sau în caz de eroare fgets returnează NULL. În caz de
succes returnează adresa şirului citit (aceeaşi cu cea primită în parametrul s).
Funcţia fputs scrie în fişierul f caracterele şirului aflat la adresa s.
Terminatorul de şir (‘\0’) nu este scris şi nici nu se adaugă caracterul newline (spre
deosebire de puts). În caz de succes fputs returnează ultimul caracter scris. În caz
de eroare returnează EOF.

Transferul de date cu format controlat este realizat prin funcţiile:

int fprintf(FILE* f,const char* format[,…]);


int fscanf(FILR* f,const char* format[,…]);

Cele două funcţii lucrează identic cu printf şi scanf. Singura diferenţă


constă în fişierul în/din care se transferă datele. Dacă printf şi scanf lucrează cu
fişierele standard stdin şi stdoud, pentru fprintf şi fscanf este necesară precizarea
explicită a fişierului cu care se lucrează, prin parametrul f.

Deşi nu lucrează cu fişiere în mod direct, se pot folosi şi funcţiile


int sprintf(char *s,const char *format[,...]);
int sscanf(const char *s,const char *format[,...]);

Aceste funcţii lucrează identic cu printf şi scanf, diferenţa constând în entitatea


din/în care se transferă datele. În locul fişierelor standard, acest funcţii folosesc o zonă
de memorie de tip şir de caractere, a cărei adresă este furnizată în parametrul s. Şirul de
la adresa s poate fi obţinut prin transfer fără format dintr-un fişier text (pentru sscanf)
sau poate urma să fie scris într-un fişier text prin funcţia fputs.

69
Programarea calculatoarelor

Pentru tratarea erorilor se folosesc următoarele funcţii:

void clearerr (FILE* f);

Funcţia resetează indicatorii de eroare şi indicatorul de sfârşit de fişier


pentru fişierul f (se înscrie valoarea 0). Odată ce indicatorii de eroare au fost setaţi
la o valoare diferită de 0, operaţiile de intrare/ieşire vor semnala eroare până la
apelul lui clearerr sau rewind.

int ferror (FILE* nume_intern);

Ferror este o macrodefiniţie care returnează codul de eroare al ultimei


operaţii de intrare/ieşire asupra fişierului nume_intern (0 dacă nu s-a produs
eroare).

Exemplu:
#include <stdio.h>
int main(void)
{ FILE *f;
/* deschide fisierul pentru scriere*/
f=fopen("test.ttt","w");
/* se produce eroare la incercarea de citire */
getc(f);
if(ferror(f)) /* s-a produs eroare de I/E? */
{/* afiseaza mesaj de eroare */
printf("Eroare al citirea din test.ttt\n");
//reseteaza indicatorii de eroare si sfarsit de
fisier
clearerr(f);}
fclose(f);
return 0;}

Exemplu:
Să se scrie un program care calculează şi afişează valoarea unei funcţii
introduse de la tastatură într-un punct dat. Funcţia se introduce ca şir de caractere şi
poate conţine apeluri de funcţii standard C (vezi şi [Smeu95]). Programul creează
un fişier sursă C (în care este scrisă forma funcţiei, ca subprogram C), apoi
compilează şi execută un alt program, care va include subprogramul creat.
Descrierea funcţiei introduse de la tastatură trebuie să conţină maxim 200
caractere.
a) Fişierul 51_iii_a.cpp conţine programul care realizează citirea formei
funcţiei, compilarea şi execuţia programului care calculează valoarea funcţiei.
#include<stdlib.h>
#include<stdio.h>
#include<conio.h>
#include<string.h>
#include<process.h>
void main()

70
Fişiere de date

{ char s1[213]="return(";
char s2[]="double f(double x)\r\n\{\r\n";
FILE *f; int n,i,j;
f=fopen("functie.cpp","w");
fputs(s2,f);
printf("functia f(x)="); gets(&s1[7]);
strncat(s1,");\r\n}",6);
fputs(s1,f);
fclose(f);
system("bcc –Id:\borlandc\include -Ld:\borlandc\lib 51_iii_b.cpp>>
tmp.txt");
execl("51_iii_b ",NULL);
}

b) Fişierul 51_iii_b conţine programul care face citeşte punctul x,


calculează valoarea funcţiei în acest punct şi o afişează.

#include<stdio.h>
#include<conio.h>
#include<math.h>
#include"functie.cpp"
void main()
{ double x;
printf("x=");scanf("%lf",&x);
printf("f(%7.2lf)=%7.2lf",x,f(x));
getch();
}

71
6 Algoritmi de prelucrare
a fişierelor binare

Din punct de vedere al operaţiilor de gestiune solicitate de diverse aplicaţii,


fişierele binare se pot grupa în: fişiere care nu sunt actualizate (ţinute la zi) şi fişiere
care sunt actualizate. De obicei, fişierele din prima grupă se regăsesc în aplicaţii
matematice sau ca fişiere temporare şi de tranzacţii în aplicaţii de gestiune
economică. Fişierele din cea de-a doua grupă sunt, de obicei, fişiere permanente
(principale) în aplicaţii de gestiune economică şi au particularităţi de proiectare,
referitoare, în special, la asigurarea ştergerii şi adăugării de articole.

6.1 Caracteristici generale ale algoritmilor


de prelucrare a fişierelor
Organizarea datelor în fişiere memorate pe medii magnetice externe
presupune proiectarea unor algoritmi specifici operaţiilor de gestiune a acestora,
denumiţi generic algoritmi de prelucrare a fişierelor de date. Datorită complexităţii
aplicaţiilor care prelucrează fişiere este recomandată aplicarea metodei
modularizării algoritmilor şi programelor. Modularizarea presupune ca, pe baza
analizei problemei, să se descompună rezolvarea ei în părţi distincte, numite
module, astfel încât fiecare dintre acestea să îndeplinească anumite funcţii.
Descompunerea se poate realiza în mai multe faze (pe mai multe niveluri), prin
metoda top-down. Criteriile de descompunere în module depind, în mare măsură,
de experienţa programatorilor. Ele se referă, în principal, la: omogenizarea
funcţiilor; utilizarea diverselor structuri de date; separarea funcţiilor de
intrare/ieşire de funcţiile de prelucrare; utilizarea unor module deja existente;
utilizarea eficientă a resurselor calculatorului (timp UC, memorie internă, periferie)
etc. Modulele se implementează în program prin subprograme interne sau externe.
De cele mai multe ori, o aplicaţie necesită existenţa mai multor fişiere

72
Algoritmi de prelucrare a fişierelor binare

active simultan, cu rol diferit (de intrare, de ieşire, de intrare/ieşire). Indiferent de


numărul fişierelor utilizate, în marea majoritate a algoritmilor, logica prelucrării
este coordonată, la un moment dat, de un singur fişier, obligatoriu de intrare,
parcurs secvenţial, numit fişier conducător (sau director). Fişierul conducător are
proprietatea că articolele lui pot fi citite logic independent de prelucrarea altor
fişiere. Altfel spus, un fişier nu este conducător dacă prelucrarea articolelor sale
este dependentă de existenţa (de citirea) articolului altui fişier. Accesul la datele
memorate în fişierul conducător se realizează la nivel de articol. De aceea,
algoritmii de prelucrare, indiferent de operaţia de gestiune, necesită utilizarea unei
structuri repetitive pentru parcurgerea (parţială sau integrală) a fişierului respectiv.
Algoritmii de prelucrare cu fişier conducător pot fi reprezentaţi prin
schema logică generalizată, concepută modularizat, redată în figura 6.1.

Figura 6.1 Schema logică generală a unui algoritm


de prelucrare cu fişier conducător

Modulul ÎNCEPUT se realizează o singură dată, înaintea prelucrării


primului articol al fişierului conducător şi cuprinde următoarele grupe de operaţii:
Operaţii iniţiale standard, obligatorii oricărui algoritm şi care includ:
punerea în corespondenţă a fişierelor logice cu fişiere fizice, deschiderea fişierelor,
şi, pentru anumite variante, iniţializarea unei variabile logice pentru sfârşit de fişier
(SF) şi citirea primului articol.
Operaţii iniţiale specifice, facultative, existenţa lor depinzând de
particularităţile problemei abordate şi care includ, în principal: iniţializări de
variabile de total, afişări ale antetului, titlului şi/sau a capului de tabel pentru
situaţii de ieşire etc.
Modulul PRELUCRARE se execută repetitiv şi cuprinde, pe de o parte,

73
Programarea calculatoarelor

totalitatea operaţiilor de prelucrare a articolului curent al fişierului conducător -


operaţii specifice fiecărei probleme - şi, pe de altă parte, citirea unui articol din
fişierul conducător. Ordinea celor două operaţii (citire şi prelucrare) depinde de
varianta de algoritm aleasă.
Modulul SFÎRŞIT se execută o singură dată, după prelucrarea ultimului
articol al fişierului conducător şi include următoarele grupe de operaţii: operaţii
finale standard, corespunzând închiderii fişierelor implicate în prelucrare; operaţii
finale specifice, care depind de natura problemei şi includ, de regulă: afişarea
variabilelor de total, a statisticilor privind operaţiile de gestiune executate,
închiderea situaţiilor de ieşire etc.
Modalitatea de detectare/tratare a sfârşitului de fişier conduce la existenţa
mai multor variante ale schemei generale de prelucrare cu fişier conducător, prin
forme particulare ale condiţiei sfîrşit_de_prelucrare. În funcţie de variantele alese,
se pot construi scheme logice valabile pentru toate tipurile de fişiere sau numai
pentru fişierele binare.

Scheme valabile pentru toate tipurile de fişiere


Detectarea sfârşitului de fişier, cu macrodefiniţia feof, caz în care testarea
sfârşitului de fişier trebuie să urmeze după o operaţie de citire a unui articol.
Algoritmul trebuie să conţină o citire iniţială în modulul ÎNCEPUT şi o citire
curentă la sfârşitul modulului PRELUCRARE - (figura 6.2). Acest algoritm se
poate aplica fişierelor vide sau nevide.

Operatii initiale

Operatii finale

Figura 6.2

Scheme logice valabile numai pentru fişiere binare

74
Algoritmi de prelucrare a fişierelor binare

Detectarea sfîrşitului de fişier prin operaţia de citire, verificând rezultatul


întors de funcţia de citire (fread). Dacă rezultatul este mai mic decât numărul de
blocuri de date care trebuie citite, înseamnă că s-a ajuns la sfârşitul fişierului.
Întrucât, uzual, la o operaţie de citire se citeşte un articol întreg, în cazul atingeri
sfârşitului de fişier, rezultatul întors de funcţia fread va fi 0. Rezultatul poate fi
preluat într-o variabilă pentru a fi folosit în condiţia de terminare a prelucrării
(figura 6.3) sau poate fi verificat direct, folosind apelul funcţiei fread în expresia
(condiţia) care controlează sfârşitul prelucrării (figura 6.4). În ambele variante
fişierul conducător este binar, vid sau nevid.

START

Operaţiiinitiale
Operatii iniţiale

SF = fread (…)

SF != 0 Da

Prelucrare articol
Nu

Operaţiifinale
Operatii finale SF = fread (…)

STOP

Figura 6.3

Operatii initiale

Operatii finale

Figura 6.4

75
Programarea calculatoarelor

Prelucrarea unui număr cunoscut de articole, prin determinarea în


modulul ÎNCEPUT a numărului de articole din fişierul conducător se regăseşte în
figura 6.5. Limbajul C nu oferă o funcţie standard pentru calcularea numărului de
articole dintr-un fişier binar, deoarece, din punctul de vedere al limbajului, fişierele
nu conţin articole. Din punctul de vedere al utilizatorului, cunoscând dimensiunea
unui articol, se poate calcula numărul de articol de fişier, împărţind lungimea
acestuia la lungimea unui articol (ambele măsurate în număr de octeţi). Lungimea
fişierului este egală cu poziţia curentă, atunci când pointerul de citire se află la
sfârşitul fişierului. Pentru aflarea numărului de articole, se foloseşte secvenţa
următoare:
p=ftell(f);
fseek(f,0,SEEK_END);
l=ftell(f);
nr=l/sizeof(tip_articol);
fseek(f,p,SEEK_SET);

unde: variabila p, de tip long reţine poziţia curentă în fişier; f este fişierul a cărui
lungime trebuie calculată; variabila l reţine poziţia curentă (în număr de octeţi faţă
de începutul fişierului, deci lungimea fişierului măsurată în octeţi); variabila nr va
primi ca valoare numărul de articole din fişier; tip_articol este tipul articolelor din
fişier (din punctul de vedere al utilizatorului). Împărţirea se face exact, deoarece
fişierul conţine un număr întreg de articole – utilizarea acestei secvenţe asupra unui
fişier care conţine articole de alt tip (sau are conţinut de altă natură) va duce la
rezultate incorecte.

Operatii initiale

Operatii finale

Figura 6.5

76
Algoritmi de prelucrare a fişierelor binare

Caracteristica generală a algoritmilor de prelucrare cu fişier conducător


este parcurgerea secvenţială a fişierului conducător şi efectuarea unor prelucrări în
funcţie de fiecare articol citit din acesta. Problema care se pune este detectarea
sfârşitului de fişier. În C, macrodefiniţia feof nu face decât să furnizeze valoarea
indicatorului de sfârşit de fişier, care este setat de operaţia de citire; în program,
citirea trebuie să apară înaintea verificării sfârşitului de fişier. Forma generală a
algoritmului este:

<citire articol>
while(!feof(f))
{ <prelucrare articol citit>
<citire articol>
}

Exemplu:
Crearea şi consultarea unui fişier text care memorează elemente întregi,
folosind funcţia feof pentru gestionarea sfârşitului de fişier. La crearea fişierului, fişier
conducător este fişierul standard de intrare. La afişare, conducător este fişierul f.
#include<stdio.h>
#include<conio.h>
void main()
{ FILE *f;
int x; long dim;
clrscr(); f=fopen("numere.dat","w+");
scanf("%d",&x);
while(!feof(stdin))
{fprintf(f,"%d\n",x); scanf("%d",&x);}
fseek(f,0,SEEK_SET);
fscanf(f,"%d",&x);
while(!feof(f))
{printf("%d\t",x);
fscanf(f,"%d",&x);}
fclose(f);
getch();}

Acelaşi exemplu, folosind fişier binar:


#include<stdio.h>
#include<conio.h>
void main()
{ FILE *f;
int x,g; long dim;
clrscr(); f=fopen("numere.dat","wb+");
scanf("%d",&x);
while(!feof(stdin))
{fwrite(&x,sizeof(x),1,f);
scanf("%d",&x);}
fseek(f,0,SEEK_SET);
fread(&x,sizeof(x),1,f);
while(!feof(f))
{printf("%d\t",x);
fread(&x,sizeof(x),1,f);}
fclose(f);
c=getch();}

77
Programarea calculatoarelor

Fişierele utilizate într-o aplicaţie informatică au rol diferit în procesul


prelucrării, în funcţie de scopul lor: de intrare, de ieşire, de intrare/ieşire,
temporare, de tip listă etc. Aceste caracteristici conduc la algoritmi specifici
fiecărei operaţii de gestiune în parte (creare, populare, consultare şi actualizare),
fiind însă variante derivate din schema generală a unui algoritm de prelucrare cu
fişier conducător. Deoarece aplicaţiile informatice din domeniul economic, social,
administrativ etc. utilizează, cu predilecţie, fişiere cu articole de aceeaşi structură
(sau un număr mic de structuri diferite), alegând limbajul C, se poate aprecia că
cele mai performante sunt fişierele binare, ale căror articole sunt date declarate ca
structuri (folosind tipul de date struct).
Această alegere este motivată din următoarele puncte de vedere:
descrierea articolelor este apropiată atât descrierii naturale a structurii unei
entităţi din lumea reală (formată din câmpuri cu nume, lungime, reprezentare
internă proprie, semnificaţie şi factor de repetabilitate diferite), cât şi descrierii din
alte limbaje;
există posibilitatea de a descrie explicit mai multe structuri pentru
articolele aceluiaşi fişier (articole cu structură variabilă);
operaţiile de acces la înregistrări se realizează cu viteză mare, datorită
lipsei conversiilor la transferul între memoria principală şi memoria externă.
Fişierele cu conţinut de tip text sunt recomandate a fi utilizate ca fişiere de
ieşire, pentru realizarea de liste, situaţii finale, rapoarte etc., fiind rezidente pe disc,
în general, până la listarea lor la imprimantă. Fişierele cu conţinut de tip text pot
constitui şi sursa de creare a fişierelor binare, dacă acestea au fost populate cu date,
fie prin editoare de texte, fie prin alte limbaje (Cobol, Fortran, Basic, Pascal),
sisteme de gestiune a bazelor de date (DBase, FoxPro, Oracle etc.), constituind
unicul mijloc de compatibilitate directă.

6.2 Algoritmi de prelucrare a fişierelor binare


care nu necesită actualizare
Asupra fişierelor binare care nu necesită actualizare se realizează, de obicei,
operaţiile de creare (populare) şi consultare. Dintre operaţiile de actualizare pot fi
realizate, fără mari complicaţii, modificarea şi adăugarea densă de articole.
Popularea fişierelor se realizează prin preluarea datelor fie din alte fişiere
primare (cu conţinut binar sau de tip text), fie de la tastatură (popularea interactivă).
În ultimul caz, cel mai des întâlnit în practică, fişierul conducător corespunde
mulţimii datelor introduse de la tastatură. Articolele sunt preluate câmp cu câmp,
neexistând posibilitatea citirii unei variabile de tip articol şi, în plus, introducerea
unei date este adesea însoţită de proceduri de validare specifice, cu reintroducerea ei
în cazul unei erori.
Sfârşitul introducerii datelor de la tastatură (şi implicit al procesului de
populare a fişierului) poate fi:
De tip chestionar, prin consultarea utilizatorului, privind continuarea sau nu a
introducerii articolelor. Pentru un volum mare de date, varianta prezintă dezavantajul

78
Algoritmi de prelucrare a fişierelor binare

măririi timpului de prelucrare.


Convenţional, prin introducerea pentru primul câmp din articol a unei valori
prestabilite, cu semnificaţie de sfârşit de prelucrare.
Standard, prin introducerea caracterului CTRL-Z, cu rol de sfârşit de fişier
text.
Schema logică a algoritmului de prelucrare este similară celei din figura 6.2.,
cu următoarele particularităţi:
• modulul ÎNCEPUT are ca ultime operaţii, afişarea numelui primului câmp
din articol şi citirea valorii sale;
• modulul PRELUCRARE începe cu citirea următorului câmp, urmată de
citirea celorlalte câmpuri (eventual cu validările stabilite) şi se termină cu afişarea
numelui primului câmp şi cu citirea valorii acestuia (pentru articolul următor) (similar
operaţiei din modulul ÎNCEPUT).
O altă problemă a populării fişierelor binare o reprezintă aşezarea articolelor
pe suportul extern. Din acest punct de vedere se întâlnesc două modalităţi:
Populare densă, prin care articolele se scriu unul după altul, în ordinea în
care au fost furnizate, fără a se lăsa locuri libere (acces secvenţial). Pentru fişierele
care nu necesită actualizare acesta este tipul recomandat.
Populare aleatoare, prin care articolele sunt scrise în casetele (virtuale) ale
căror numere relative sunt furnizate explicit de utilizator (acces direct). Scrierea unui
articol se realizează după poziţionarea pe numărul relativ dorit. La populare,
nr_relativ nu este limitat decât de spaţiul existent pe suportul extern. Metoda are
dezavantajul că necesită evidenţa "articolelor vide". În cazul fişierelor care nu
necesită actualizare, popularea aleatoare se recomandă numai dacă, după creare,
fişierul este dens.
Pentru poziţionarea pe articolul cu numărul relativ n se foloseşte funcţia fseek
astfel:

fseek(f, n*sizeof(tip_articol), SEEK_SET);

unde n este numărul relativ al articolului iar tip_articol este tipul de dată care îi
corespunde.

Exemplu:
1. Să se creeze cu populare densă un fişier PRODUSE.DAT cu informaţii despre
producţia cantitativă într-un an, la o societate comercială. Articolele au următoarea
structură logică:

Cod Denumire Preţ


Cantităţi lunare
produs Produs Mediu
1 2 ... 12

Articolele sunt introduse de la terminal, câmp cu câmp. Terminarea introducerii


datelor este marcată standard, prin introducerea caracterului CTRL-Z.

79
Programarea calculatoarelor

#include <stdio.h>

typedef struct { int cod;


char denumire[20];
float pret_mediu;
int cant[12];
} PRODUS;

void main()
{ FILE* f;
PRODUS x;
char nume_fisier[20];
int i;

//---INCEPUT---
printf("\n\nNumele fisierului: ");
gets(nume_fisier);
if(!(f=fopen(nume_fisier,"wb"))) printf("\n\nNu poate fi creat
fisierul cu numele %s",nume_fisier);
else
{ printf("\nCod produs: ");
scanf("%d",&x.cod);
//---Aici se termina operatiile initiale---

while(!feof(stdin))
{
//---PRELUCRARE ARTICOL---
printf("Denumire produs: ");
fflush(stdin);
gets(x.denumire);
printf("Pret mediu: ");
scanf("%f",&x.pret_mediu);
printf("Cantitate lunara:\n");
for(i=0;i<12;i++)
{ printf(" - luna %d: ",i+1);
scanf("%d",&x.cant[i]);
}
fwrite(&x,sizeof(PRODUS),1,f);
//---Aici se incheie prelucrarea articolului---
printf("\nCod produs: ");
scanf("%d",&x.cod);
}
//---SFIRSIT---
fclose(f);
}
}

Observaţii: Dacă se doreşte crearea fişierului de date cu populare în acces direct,


programul este similar, cu următoarele diferenţe:
câmpul COD indică numărul relativ al articolului în fişier şi nu va fi memorat
(nu va face parte din declaraţia tipului PRODUS), fiind redundant;
scrierea articolului va fi precedată de apelul funcţiei
fseek(f,codt*sizeof(PRODUS),SEEK_SET);
unde codt este o variabilă independentă în care se citeşte codul de la terminal.
Consultarea fişierelor are numeroase variante, în funcţie de scopul
prelucrării. După modul de regăsire a articolelor în cadrul fişierului, ea poate fi
secvenţială, directă sau mixtă.

80
Algoritmi de prelucrare a fişierelor binare

Consultarea secvenţială presupune regăsirea articolelor în ordinea în care au


fost scrise pe suportul tehnic de informaţii. După numărul articolelor prelucrate,
consultarea secvenţială poate fi: integrală, când se prelucrează toate articolele
fişierului, începând cu primul şi terminând cu ultimul; cu selecţie, când se prelucrează
numai acele articole care au una sau mai multe caracteristici comune (valori identice
pentru acelaşi câmp). După numărul de caracteristici, selecţia poate fi simplă, dublă,
multiplă. Pentru consultarea secvenţială se poate utiliza oricare din tipurile de
algoritmi prezentaţi anterior.

Exemplu:
2. Să se afişeze pe ecran conţinutul fişierului creat la exemplul 1.

#include <stdio.h>
typedef struct { int cod;
char denumire[20];
float pret_mediu;
int cant[12];
} PRODUS;
void main()
{ FILE* f;
PRODUS x;
char nume_fisier[20];
int i;
//---INCEPUT---
printf("\n\nNumele fisierului: ");
gets(nume_fisier);
if(!(f=fopen(nume_fisier,"rb"))) printf("\n\nNu poate fi deschis
fisierul cu numele %s",nume_fisier);
else
{ fread(&x,sizeof(PRODUS),1,f);
//---Aici se termina operatiile initiale---
while(!feof(f))
{
//---PRELUCRARE ARTICOL---
printf("\n\nCod produs:\t\t%d",x.cod);
printf("\nDenumire produs:\t%s",x.denumire);
printf("\nPret mediu:\t\t %7.2f",x.pret_mediu);
printf("\nCantitati lunare:\t");
for(i=0;i<12;i++)
printf("%3d ",x.cant[i]);
//---Aici se incheie prelucrarea articolului---
fread(&x,sizeof(PRODUS),1,f);
}
//---SFIRSIT---
fclose(f);
}
}

3. Obţinerea unei situaţii cu mai multe grade de total. Pentru aceasta se stabilesc
câmpuri asociate gradelor de total, numite caracteristici de grupare sau caracteristici
de control. O caracteristică de control este un câmp al articolului din fişierul de date,
care are aceeaşi valoare pentru mai multe înregistrări. Astfel, articolele care au
valoare comună pentru o caracteristică de grupare se pot ordona pe submulţimi,
formând o grupă de control. Fişierul poate constitui, în ansamblul său, caracteristica

81
Programarea calculatoarelor

de grupare de cel mai înalt nivel, pentru care se poate calcula totalul general.
Numărul maxim de grade de total este superior cu unu numărului de
caracteristici de control stabilite. Între caracteristicile de grupare se stabileşte o relaţie
de ordine ierarhică. Pentru prelucrarea fişierului, cu utilizare minimă de memorie,
articolele trebuie sortate după caracteristicile de control. Acest tip de prelucrare intră
în categoria consultărilor secvenţiale integrale şi urmează algoritmul de principiu din
figurile 6.2 sau 6.3. Prelucrarea unui fişier sortat după criteriile enunţate, presupune
existenţa unor operaţii standard, executate la schimbarea valorii fiecărei caracteristici
de control stabilite:
operaţii iniţiale ale unei grupe de control prin care se iniţializează variabila
de total specifică grupei; se salvează valoarea caracteristicii primului articol din
grupă; alte operaţii iniţiale specifice grupei;
operaţii finale ale unei grupe de control prin care se afişează totalul calculat
pentru caracteristica ce se schimbă; se cumulează totalul grupei curente la totalul
grupei ierarhic superioare; alte operaţii finale specifice grupei;
condiţia de prelucrare a unei grupe de control conţine, pe lângă condiţia
specifică, toate celelalte condiţii din amonte.
Raportul final este listat la imprimantă, fie direct, ca fişier de ieşire, fie creat
pe suport magnetic, ca fişier text, în vederea imprimării ulterioare. Structura unei
pagini a raportului şi controlul trecerii la o nouă pagină trebuie asigurate de
programator.
În continuare (figura 6.6) se prezintă structura de principiu a unui program de
obţinere a unui raport final, cu control după două caracteristici şi trei grade de total,
unde cîmp_1 şi cîmp_2 sunt caracteristicile de control (câmpuri din articol), v1 şi v2
sunt variabile de lucru pentru salvarea caracteristicilor, val_art e valoarea care
interesează din fiecare articol (câmp al articolului sau valoare calculată pe baza unor
câmpuri ale articolului), iar TOTG, TOT1 şi TOT2 sunt variabile pentru calculul
gradelor de total. Analog, se poate extinde pentru oricâte caracteristici şi grade de
total.

82
Algoritmi de prelucrare a fişierelor binare

START

Operaţii iniţiale
generale

TOTG=0

Citeşte
articol

!feof(f) Da

Operaţii iniţiale
Grupa 1

TOT1=0
v1=cîmp_1

! feof(f) şi
Da
v1==cîmp_1
Operaţii iniţiale
Nu Grupa 2
Operaţii finale
Geupa 1 TOT2=0
v2=cîmp_2
TOTG+=TOT1
! feof(f) şi
v1==cîmp_1 şi Da
v2==cîmp_2
Prelucrare articol

Operaţii finale Operaţii finale


generale Nu Grupa 2 TOT2+=val_art

TOT1+=TOT2
Citeşte
STOP articol
Nu

Figura 6.6 Schema logică – problema cu grade de total

Consultarea în acces direct presupune regăsirea articolului după numărul


relativ. Întrucât fişierele sunt considerate ca fluxuri de octeţi, trebuie calculată poziţia
articolului dorit în fişier ca produs între numărul său relativ şi dimensiunea unui
articol în octeţi (nr*sizeof(tip_articol)). Secvenţa care realizează acest lucru este:

fseek(f,nr*sizeof(tip_articol), SEEK_SET);
fread(&art,sizeof(tip_articol), 1, f);

83
Programarea calculatoarelor

Numărul relativ este furnizat de utilizator şi trebuie să aparţină domeniului


0..dimensiune fişier-1 (dimensiunea calculată ca număr de articole). Pentru evitarea
situaţiilor în care numărul relativ se află în afara acestui domeniu, se va include în
program validarea apartenenţei numărului relativ la intervalul acceptat. Algoritmul de
consultare în acces direct a unui fişier are un alt fişier conducător (de exemplu
tastatura).

Exemplu:
4.
{
// citire nume fisier extern
f=fopen(nume_fisier, "rb");
// calculare numar de articole din fisier
printf("\nNr. relativ: ");
scanf("%d",&r); //citirea numarului relativ al articolului
while(!feof(stdin))
{ if(r>=nr_art) printf("\n Articol inexistent !");
else
{ fseek(f,r*sizeof(tip_articol),SEEK_SET);
fread(&art,sizeof(tip_articol),1,f);
// ------------------------
//PRELUCRARE ARTICOL
//------------------------
}
printf("\nNr. Relativ (sau CTRL-Z): ");
scanf("%d",&r);
}
fclose(f);
}

Consultarea în acces mixt utilizează o combinaţie între accesul direct şi cel


secvenţial, în vederea prelucrării unui grup de articole, memorate contiguu în fişier şi
selectabile printr-o condiţie. Pentru fişierele binare, metoda poate fi aplicată dacă se
doreşte selectarea articolelor dintre două limite ale numerelor relative (limita
inferioară - li şi limita superioară - ls). Algoritmul trebuie să verifice relaţia
0≤li≤ls≤dimensiune fişier, după care parcurgerea fişierului poate fi realizată prin
orice tip de structură repetitivă.

Exemplu:
5.

{
// citire nume fisier extern
f=fopen(nume_fisier, "rb");
// calculare numar articole din fisier
printf("\nLimita inferioara: ");
scanf("%d",&li); // citirea nr. relativ al primului articol
// din secventa
printf("\nLimita superioara: ");
scanf("%d",&ls); // citirea nr. relativ al ultimului articol
// din secventa
if((0<li)&&(li<=ls)&&(ls<=nr_art))
{ fseek(f,li*sizeof(tip_articol),SEE_SET);
for(i=li;i<=ls;i++)

84
Algoritmi de prelucrare a fişierelor binare

{ fread(&art,sizeof(tip_articol),1,f);
// -----------------------
// Prelucrare articol
// -----------------------
}
}
else printf(" Nu este indeplinita conditia de limite");
fclose(f)
}

Adăugarea de articole se realizează, în general, cu tranzacţii de la terminal,


similar operaţiei de populare. Pentru o corectă exploatare ulterioară, adăugarea
trebuie să fie densă. Acest lucru poate fi realizat astfel:
Adăugare la sfârşit (extindere), după ultimul articol scris. Operaţia se
realizează similar populării în acces secvenţial, după poziţionarea pe marcatorul de
sfârşit de fişier, apelând funcţia fseek:
fseek(f,0,SEEK_END);

Exemplu:
6.
{ // citire nume fisier extern
f=fopen(nume_fisier, "rb");
fseek(f,0,SEEK_END); // pozitionare dupa ultimul
// articol scris
printf("Cimp 1: ");
scanf("%d ",&art.cimp_1);
while(!feof(stdin))
{ // -----------------------------------------------
// Preluare de la tastatura a celorlalte
// campuri din articol
// -----------------------------------------------
printf("Cimp 1: ");
scanf("%d ",&art.cimp_1);
}
fclose(f)
}

Inserarea unor articole. Se aplică în cazul în care articolele sunt scrise în


fişier în ordinea crescătoare (descrescătoare) a valorilor unui anumit câmp. În acest
caz, noul articol va fi inserat între două articole, astfel:
se caută (cu un algoritm secvenţial, binar etc.) poziţia k în care trebuie inserat
noul articol;
se copiază, glisând cu o poziţie spre dreapta, toate articolele de la sfârşitul
fişierului până la articolul cu numărul relativ k;
se scrie în acces direct noul articol, în poziţia k.

85
Programarea calculatoarelor

Exemplu:
7.
{ // articolele fisierului sunt in ordinea
// crescatoare a valorii campului 1
// citire nume fisier extern *)
f=fopen(nume_fisier, "rb+ ");

// calculare numar de articole din fisier


printf("\nCimp 1: "); // introducerea campului dupa care
// sunt sortate articolele
scanf("%d ",&art_nou.cimp_1);
while(!feof(stdin)) // adaugarea mai multor articole
{ // -----------------------------------
// Preluare de la tastatura a celorlalte
// campuri din articolul de adaugat
// -----------------------------------
// secventa de cautare a pozitiei
//in care se va insera articolul }
fseek(f,0,SEEK_SET); // pozitionare pe inceput de fisier
fread(&art_existent,sizeof(tip_articol),1,f);
while((!feof(f))&&(art_existent.cimp_1<art_nou.cimp_1)
fread(&art_existent,sizeof(tip_articol),1,f);
if(!feof(f))
{ k=ftell(f)-sizeof(tip_articol); //articolul se va
// insera in pozitia k
for(i=nr_art-1;i>=k;i--)
{ fseek(f,i*sizeof(tip_articol),SEK_SET);
fread(&art_existent,sizeof(tip_articol),1,f);
fwrite(&art_existent,sizeof(tip_articol),1,f);
}
}
else k=nr_art; // articolul se adauga la sfirsitul
// fisierului
fseek(f,k*sizeof(tip_articol),SEK_SET);
fwrite(&art_nou,sizeof(tip_articol),1,f);
printf("\nCimp 1: "); // introducerea campului dupa care
// sunt sortate articolele
scanf("%d ",&art_nou.cimp_1);rite('Camp 1: ')
}
fclose(f);
}

Modificarea valorii unor câmpuri din articol se realizează în mai multe


etape:
se citeşte articolul care se modifică (fseek şi fread);
se modifică (în zona articol din memoria principală) câmpurile cu valorile
dorite, introduse, în general, de la tastatură;
se repoziţionează pe articolul respectiv cu
fseek(f,ftell(f)-sizeof(tip_articol),SEEK_SET);
se scrie articolul modificat, cu funcţia fwrite.
O problemă importantă rămâne selectarea câmpurilor care se modifică,
pentru fiecare articol în parte.
O variantă simplă este afişarea vechii valori, urmată de introducerea noii
valori, în variabile independente de tip şir de caractere. În cazul în care şirul introdus
este vid (s-a apăsat numai ENTER), respectivul câmp, prin convenţie, nu se modifică.

86
Algoritmi de prelucrare a fişierelor binare

Altfel, câmpului respectiv i se va atribui valoarea citită în variabila independentă,


eventual prin conversie, pentru câmpurile numerice.

Exemplu:
8.
// ------------------------------
// cautare articol de modificat
// ------------------------------ *)
fread(&art,sizeof(tip_articol),1,f);
printf("Codul: %d - ",art.cod); //afisare vechea valoare
fflush(stdin);
gets(cods); // citire noua valoare; cods este de tip sir
if(strlen(cods))
{ art.cod=atoi(cods); // conversie din ASCII in binar
printf("Denumire: %s - ",art.den); // afisare vechea
// valoare
gets(dens); // citire noua valoare
if(strlen(dens)
strcpy(dens,art.den); // copiere noua valoare
// ----------------------------------
// Introducerea celorlalte campuri
// din articol
// ----------------------------------
// repozitionare pe articol
feek(f,ftell(f)-sizeof(tip_articol),SEEK_SET);
// rescriere articol modificat
fwrite(&art,sizeof(tip_articol),1,f);
}

O altă variantă se poate realiza prin folosirea unei machete de ecran în care se
afişează valorile actuale ale fiecărui câmp de modificat, se poziţionează succesiv
cursorul la începutul fiecărui câmp, cu două răspunsuri posibile ale utilizatorului:
<ENTER>, caz în care se menţine actuala valoare, respectiv o tastă diferită de
<ENTER>, reprezentând primul caracter al noii valori.

6.3 Algoritmi de prelucrare a fişierelor binare


care necesită actualizare
Prelucrarea fişierelor binare care necesită actualizare trebuie să asigure
posibilitatea ştergerii articolelor şi să elimine riscul de suprascriere a articolelor
adăugate. Pentru aceasta, trebuie proiectate structuri particulare de articole şi
concepute operaţii de gestiune specifice.
Fără a epuiza multitudinea soluţiilor de rezolvare a problemelor de gestiune a
fişierelor care necesită actualizare, în continuare, se prezintă câteva soluţii posibile. În
orice situaţie, limitările de regăsire prin acces secvenţial sau relativ a articolelor în
fişier reduc aria folosirii limbajului în probleme de gestiune. Marele inconvenient îl
constituie lipsa accesului după cheie, cel care corespunde cel mai bine gestiunii în
sistemele reale.

87
Programarea calculatoarelor

În cele ce urmează se analizează trei tipuri de probleme: probleme în care se


utilizează asocierea externă a numărului relativ la articolul corespunzător (codificare
prin număr relativ); probleme în care se utilizează asocierea internă a numărului
relativ la articolul corespunzător, iar acesta poate emana extern (se generează
nomenclatoare după fiecare actualizare de fişier); probleme în care se utilizează
extern coduri (chei) şi intern numere relative.

6.3.1 Probleme care utilizează codificarea externă prin numere relative

Nomenclatorul de articole conţine numărul relativ al fiecăruia dintre ele.


Nomenclatorul este elaborat extern (automat sau neautomat). Orice operaţie de
regăsire în acces relativ presupune introducerea din exterior a numărului relativ. La
crearea iniţială, fiecare articol este înscris la numărul său relativ predefinit. Asigu-
rarea ştergerii şi adăugării controlate poate fi făcută în diverse moduri:

Extinderea articolelor logice cu un indicator de stare (un octet),


ajungându-se la forma din figura 6.8.

IS Articol propriu-zis

Figura 6.8 Structura articolului care include indicatorul de stare

Indicatorul de stare (notat IS) poate lua una din cele două valori posibile (de
exemplu 0 pentru articol inactiv – inexistent sau şters, 1 pentru articol prezent). Cu
această convenţie, operaţiile de acces la articole se realizează în următoarele condiţii:
scrierea în fişier este permisă numai pentru articolele cu IS=0; citirea din fişier este
permisă numai pentru articolele cu IS=1.
Preformarea presupune deschiderea fişierului ca nou (crearea unui fişier
nou) şi scrierea unui număr de articole (la limită, zero) cu IS=0. Includerea operaţiei
de preformare conduce la dispariţia distincţiei dintre populare şi adăugare. Datorită
faptului că fişierul se deschide ca existent, orice operaţie de scriere a unui nou articol
se tratează ca adăugare. Într-un sistem de programe, deschiderea cu modul wb a unui
fişier se realizează o singură dată, în procedura de preformare.
Scrierea în acces direct presupune furnizarea numărului relativ (nr) al artico-
lului. În funcţie de valoarea lui nr se disting următoarele situaţii:
- dacă nr<dimensiune fişier, se citeşte articolul respectiv din fişier şi
adăugarea este permisă numai dacă IS=0;
- dacă nr>=FileSize(f), are loc extinderea fişierului cu preformarea
articolelor cu numerele relative cuprinse în domeniul dimensiune fişier..nr-1. Noul
articol se scrie pe poziţia nr.
Se remarcă faptul că scrierea în acces direct permite preformarea iniţială cu
zero articole.
Scrierea în acces secvenţial se face fără verificare de existenţă. Scrierea are

88
Algoritmi de prelucrare a fişierelor binare

loc în poziţia dată de pointerul curent. Procedura face IS=1. Utilizarea ei se


recomandă numai la popularea densă.
Citirea în acces direct presupune furnizarea numărului relativ (nr). Ea
verifică dacă IS=1.
Citirea în acces secvenţial analizează articolele începând cu cel de la
pointerul curent. Articolele cu IS=0 sunt ignorate, până la întâlnirea primului articol
cu IS=1 sau până se ajunge la sfârşit de fişier.
Ştergerea se realizează în acces direct. Ea presupune citirea articolului şi,
dacă ştergerea este permisă (IS=1), se modifică indicatorul de stare (IS=0) şi se scrie
articolul pe vechiul loc.
Rescrierea realizează scrierea unui articol în poziţia ftell(f)-
sizeof(tip_articol), dacă vechiul articol din această poziţie are IS=1.
Folosirea articolului zero ca tabelă de ocupare în fişier. Fiecărui articol
din fişier îi corespunde câte un octet în primul articol: articolului cu numărul relativ i
îi corespunde octetul a[i]. Primul articol are structura char a[max], unde max este o
constantă care indică numărul maxim de articole pe care le poate avea fişierul pe
durata existenţei sale. Dacă articolul i este prezent, a[i]=1; dacă articolul i este inactiv
(inexistent sau şters), a[i]=0.
Cu această structură, operaţiile de acces la articole se realizează în
următoarele condiţii: scrierea în fişier a articolului cu numărul relativ i este permisă
numai dacă a[i]=0; citirea din fişier a articolului cu numărul relativ i este permisă
numai dacă a[i]=1.
Ştergerea articolului i presupune verificarea existenţei sale (a[i]=1) şi realiza-
rea operaţiei a[i]=0. Adăugarea unui articol i presupune verificarea inexistenţei lui
(a[i]=0), înscrierea articolului şi realizarea operaţiei a[i]=1.
Utilizarea acestei modalităţi necesită încărcarea iniţială în memoria principală
a articolului cu numărul relativ zero. În programele care realizează ştergeri sau/şi
adăugări, înainte de închiderea fişierului trebuie rescris articolul zero în fişier.
Datorită restricţiei impuse pentru numărul de articole din fişier, acest model
de gestiune a articolelor este ineficient pentru multe probleme. Se pot concepe
algoritmi prin care în tabela de ocupare în fişier fiecărui articol îi corespunde un bit,
în loc de un octet. În acest fel numărul maxim de articole ce pot fi adăugate în fişier
se măreşte de 8 ori.

6.3.2 Probleme care utilizează codificarea internă prin numere relative

Înscrierea articolelor în fişiere, chiar cea iniţială, se face în primele articole


inactive, asociindu-se astfel intern un număr relativ fiecărui articol.
Deosebirea esenţială între această soluţie şi cea prezentată anterior, constă în
modul de realizare a adăugării secvenţiale. După fiecare sesiune de actualizare va
trebui listat nomenclatorul de coduri interne (numere relative). Articolelor din fişier li
se asociază structura din figura 6.8. Preformarea, consultarea, modificarea şi ştergerea
sunt similare celor prezentate în §6.3.1. Adăugarea unui articol se realizează în

89
Programarea calculatoarelor

condiţii diferite, după cum această operaţie are loc printre articolele existente,
respectiv după ultimul articol (extindere). În ambele situaţii, condiţiile
de realizare sunt determinate de modul de acces folosit: secvenţial sau direct.
Adăugarea în acces secvenţial se bazează pe presupunerea că utilizatorul nu
impune o corespondenţă prestabilită între conţinut şi numărul articolului, adică în alţi
termeni, că se acceptă o codificare automată. Adăugarea în acces secvenţial poate fi
utilizată în două variante:
Cu verificarea existenţei de articole libere, caz în care se adaugă noul articol
în prima poziţie găsită disponibilă (IS=0), eventual la sfârşit (extindere), dacă nu mai
există articole libere în interior. Această variantă presupune existenţa unei soluţii de
gestiune a articolelor libere (în urma preformării sau a ştergerii logice). Dintre
soluţiile posibile pot fi menţionate:
Folosirea articolului zero pentru colectarea numărului articolelor libere,
într-o structură de forma celei din figura 6.9.

nal al[1] al[2] ... al[nal]


Articolul 0
WORD WORD WORD ... WORD

Figura 6.9 Structura articolului zero, pentru gestiunea articolelor libere

În această soluţie, nal este numărul articolelor libere şi al[i], cu i=1..nal,


reprezintă poziţiile relative ale articolelor libere. Soluţia prezintă avantajul timpului
redus de căutare şi de atribuire a unei poziţii pentru noul articol. Numărul de articole
libere ce pot fi gestionate în acest mod este limitat de descrierea articolelor principale
ale fişierului. De exemplu, dacă articolul principal are 128 de octeţi, această soluţie
permite gestionarea a 63 de articole libere (dacă se impune pentru articolul zero
aceeaşi lungime ca şi pentru celelalte articole; pentru primul articol se poate accepta
şi o dimensiune diferită – mai mare – dar nu cu mult mai mare şi oricum este o
dimensiune stabilită de la început, care nu mai poate fi mărită la nevoie). La ştergerea
logică a unui articol se realizează incrementarea valorii lui nal, iar al[nal] primeşte ca
valoare numărul relativ al articolului şters.
Folosirea articolului zero ca început al unei liste simple (sau dublu) înlănţuite a
articolelor libere, într-o structură de principiu de forma celei din figura 6.10.

pal ual

0 au 0 au ... 0 au
...

Figura 6.10 Gestionarea articolelor libere prin liste

90
Algoritmi de prelucrare a fişierelor binare

În această soluţie, articolul zero punctează pe primul (pal) şi pe ultimul (ual)


articol liber, iar fiecare articol punctează pe următorul (au).
Numărul articolelor libere care pot fi gestionate în acest mod este oarecare.
La adăugarea unui nou articol, se verifică dacă există articole libere (pal<>0) şi dacă
există, se atribuie primul articol din listă articolului de adăugat, actualizându-se
componenta articolului zero. La ştergerea unui articol, trebuie asigurată includerea sa
în lista articolelor libere, operaţia fiind posibilă la oricare din capetele listei.
Căutarea secvenţială a primului articol liber, fără organizarea unei gestiuni a
acestora. Deşi mai costisitoare ca timp de căutare, aceasta este soluţia cea mai simplă sub
aspectul programării, eliminând necesitatea unei structuri distincte a articolului zero şi
operaţiile legate de întreţinerea colecţiei sau listei articolelor libere.
În concluzie, este de preferat ultima variantă atunci când timpul de căutare nu
este prohibitiv.
Fără verificarea existenţei de articole libere, caz în care articolul este adăugat
direct la sfârşit (extindere). Această variantă este avantajoasă când are loc
introducerea de la început a majorităţii articolelor. Ea poate fi asociată cu preformarea
cu zero articole, fiecare sesiune de adăugare de noi articole fiind realizată prin
extinderea fişierului.

Adăugarea în acces direct presupune o codificare anterioară (preluarea


numărului relativ din nomenclatorul editat după fiecare creare/adăugare secvenţială)
şi se realizează identic cu operaţia de scriere directă prezentată în §6.3.1.

6.3.3 Probleme care utilizează corespondenţa internă


dintre chei şi numere relative

Majoritatea aplicaţiilor de gestiune economică utilizează fişiere de date în


care articolele trebuie regăsite după valorile unui câmp de identificare, numit cheie.
Problema corespondenţei între chei şi numerele relative ale articolelor din fişierul de
date se poate rezolva prin intermediul unui fişier binar suplimentar, cu rol de tabelă
de indexuri. O astfel de organizare se numeşte indexată. Articolele fişierului tabelă de
indexuri au structura din figura 6.11.

IS Cheie Număr relativ (nr)

Figura 6.11 Structura articolului din tabela de indexuri

Indicatorul de stare (IS) are rol identic cu cel prezentat în §6.3.1. Cheie este
un câmp în care se memorează valoarea cheii articolului existent logic în fişierul de
date, al cărui număr relativ corespunzător este memorat în câmpul nr. Articolele
tabelei de indexuri sunt, în orice moment, sortate crescător după valorile câmpului
cheie. Articolele din fişierul de date sunt memorate aleator. O parte dintre acestea nu-
şi regăsesc corespondent în tabela de indexuri, fiind considerate şterse. Orice operaţie
de acces la articolele fişierului de date se realizează numai prin intermediul tabelei,

91
Programarea calculatoarelor

gestionată automat de funcţiile unei biblioteci specializate şi care este netransparentă


utilizatorului (bibliotecă utilizator, nu face parte din limbaj).
Operaţiile de acces la nivel de fişier sunt deschiderea şi închiderea fişierului
de date. La rândul ei, deschiderea poate fi pentru creare (ca fişier nou) sau pentru
consultare şi întreţinere (ca fişier vechi). Procedurile realizează, pe lângă operaţiile
asupra fişierului de date şi gestionarea automată a tabelei de indexuri: formarea
numelui său extern, asignarea numelui fizic la numele logic, deschiderea ca fişier nou
sau vechi, închiderea. Operaţiile de gestiune ce se pot realiza cu fişierele de date
astfel organizate sunt:
Crearea în acces secvenţial presupune furnizarea articolelor sortate strict
crescător după valorile câmpului ales drept cheie. Articolele sunt scrise cu ajutorul
funcţiei de scriere în acces secvenţial. Eroarea de cheie invalidă poate apărea la
tentativa de scriere a unui articol a cărui cheie este mai mică sau egală decât ultima
înscrisă în fişierul de date.
Crearea în acces direct se realizează cu funcţia de scriere în acces direct,
articolele fiind furnizate în orice ordine. Eroarea de cheie invalidă apare la tentativa
de scriere a unui articol a cărui cheie este egală cu una din cele prezente în fişier.
Consultarea în acces secvenţial presupune regăsirea articolelor în ordinea
strict crescătoare a valorilor cheii. Ea se realizează cu ajutorul funcţiei de citire în
acces secvenţial, care detectează şi sfârşitul fişierului de date.
Consultarea în acces mixt permite selectarea unui grup de articole, memo-
rate logic contiguu în fişier, selecţie realizată pe baza valorilor cheii primului şi
ultimului articol din grupul dorit. Accesul mixt presupune poziţionarea pe primul
articol prin citirea în acces direct (sau poziţionare şi citire în acces secvenţial), urmată
de exploatarea în acces secvenţial, până la găsirea cheii ultimului articol dorit, sau
până la sfârşitul fişierului.
Adăugarea de articole se realizează utilizând funcţia de scriere în acces
direct. Articolele pot fi furnizate în orice ordine a valorilor cheii. Eroarea de cheie
invalidă apare la tentativa de scriere a unui articol a cărui cheie este egală cu una deja
existentă.
Modificarea unor câmpuri din articol se realizează în următoarele etape:
citirea articolului de modificat (în acces secvenţial sau direct);
modificarea, în zona articol corespunzătoare, a câmpurilor dorite, cu
excepţia câmpului cheie;
rescrierea articolului, cu procedura de rescriere.
Eroarea de cheie invalidă apare în cazul tentativei de rescriere a unui articol a
cărui cheie este diferită de cea a articolului citit anterior.
Ştergerea în acces secvenţial elimină articolul curent din fişierul de date. În
general, articolul trebuie mai întâi identificat printr-o citire (în acces secvenţial sau
direct) sau prin poziţionare. Procedura returnează eroare în cazul tentativei de ştergere
după ultimul articol existent.
Ştergerea în acces direct elimină articolul a cărui cheie este precizată.
Operaţia nu trebuie precedată de citirea articolului şi returnează eroare în cazul

92
Algoritmi de prelucrare a fişierelor binare

furnizării unei chei inexistente în fişier. În aplicaţii, este preferabilă ştergerea în acces
secvenţial, deoarece permite (datorită citirii care o precede), vizualizarea articolului şi
luarea unei decizii în condiţii de siguranţă.

Exemplu:
9. Exemplul următor descrie funcţii, tipuri de date şi variabile publice pentru
prelucrarea unui fişier organizat indexat. Pentru aceste exemplu, fişierul de date este
format din articole cu următoarea structură:

cheie denumire preţ cantitate

Figura 6.12 Structura articolului din tabela de indexuri

Exemplul poate fi adaptat pentru orice altă structură, modificând


corespunzător structura articolului. În acest exemplu, fişierul index va fi o variabilă
globală, accesibilă tuturor subprogramelor. Tipurile definite sunt următoarele:
typedef struct{ char cheie[7];
char den[35];
float pu;
float cant;
} ARTICOL; //tipul articol din fisierul de date

typedef struct{ char is;


char cheie[7];
long nr_rel;
} ART_INDEX; //tipul articol din tabela de indexuri

FILE* ind; //fisierul index


char nume_index[20]; //numele extern al fisierului index

Pentru implementarea operaţiilor de gestiune specifice unui fişier organizat


indexat sunt necesare următoarele subprograme:

¾ Funcţia de deschidere a tabelei index ca fişier nou, cu prototipul


void new_index(char *nume);
Funcţia primeşte ca parametru numele extern al fişierului de date (nume) şi
creează un fişier nou, tabela de indexuri, cu extensia .idx.

¾ Funcţia de deschidere a tabelei de indexuri, pentru consultare şi


întreţinere, cu prototipul
void open_index(char *nume);
Funcţia primeşte ca parametru numele extern al fişierului de date (nume), şi
deschide ca existentă tabela de indexuri, cu extensia .idx.

¾ Funcţia de închidere a tabelei de indexuri, cu prototipul


void closeindex();

93
Programarea calculatoarelor

Funcţia realizează închiderea tabelei de indexuri asociate fişierului de date.

¾ Funcţia pentru citirea în acces secvenţial a unui articol din fişierul de date,
cu prototipul
int ReadSec(fisier f,articol *a);
Funcţia are ca parametri numele intern al fişierului de date şi adresa unde
se depune articolul citit, dacă acest lucru este posibil şi returnează
- 1, dacă citirea a fost posibilă;
- 0, în caz contrar.
Citirea unui articol din fişierul de date este realizată prin intermediul
tabelei de indexuri, astfel: este citit un articol din tabelă, de la poziţia curentă a
pointerului de fişier şi apoi este citit articolul cu numărul relativ dat de câmpul
nr_rel al articolului citit din fişierul de indexuri. Dacă, în tabela de indexuri,
pointerul de fişier indică sfârşitul de fişier, atunci citirea nu este posibilă şi funcţia
returnează valoarea 0. Prin apelul repetat al funcţiei ReadSec, dacă tabela de
indexuri are poinetrul plasat înaintea primului articol, sunt obţinute articolele din
fişierul de date în ordinea strict crescătoare a valorii cheii.

¾ Funcţia pentru citirea în acces direct a unui articol din fişierul de date, cu
prototipul
int ReadKey(fisier f,articol *a,char *Key);
Funcţia are ca parametri numele intern al fişierului de date, adresa unde se
depune articolul citit, dacă acest lucru este posibil, precum şi cheia articolului care
va fi citit şi returnează
- 1, dacă citirea a fost posibilă;
- 0, în caz contrar.
Funcţia apelează modulul de căutare binară în tabela de indexuri a cheii
Key, SeekKey. Atunci când cheia este găsită, citeşte articolul cu numărul relativ
corespunzător articolului din tabela de indexuri şi returnează valoarea 1, altfel
returnează valoarea 0.

¾ Funcţia pentru scrierea în acces secvenţial a unui articol în fişierul de date,


cu prototipul
int WriteSec(fisier f,articol a);
Funcţia are ca parametri numele intern al fişierului de date şi articolul ce va
fi scris, dacă acest lucru este posibil, şi returnează
- 1, dacă scrierea a fost posibilă;
- 0, în caz contrar.
Funcţia adaugă un articol în fişierul de date, concomitent cu extinderea
tabelei de indexuri cu o nouă înregistrare, a cărei cheie este mai mare decât cele
existente. În cazul în care cheia este mai mică sau egală cu a ultimului articol din
tabelă, este returnată valoarea 0, corespunzătoare situaţiei în care scrierea nu este
posibilă.

94
Algoritmi de prelucrare a fişierelor binare

¾ Funcţia pentru scrierea în acces direct a unui articol în fişierul de date, cu


prototipul
int WriteKey(fisier f,articol a);
Funcţia are ca parametri numele intern al fişierului, articolul ce va fi scris,
dacă acest lucru este posibil, şi returnează
- 1, dacă scrierea a fost posibilă;
- 0, în caz contrar.
Funcţia adaugă un articol la sfârşitul fişierului de date. Cheia acestuia,
a.cheie, poate avea orice valoare (care nu există deja în tabela de indexuri). Iniţial,
tabela se extinde cu un nou articol şi apoi este reordonată (prin apelul funcţiei
Sort). În cazul în care cheia articolului de scris este deja prezentă în tabela de
indexuri, articolul nu este scris în fişier şi funcţia returnează valoarea 0. Căutarea
cheii în tabela de indexuri pentru stabilirea posibilităţii scrierii este realizată prin
apelul funcţiei SeekKey.

¾ Funcţia pentru ştergerea în acces secvenţial a unui articol, cu prototipul


int DeleteSec();
Funcţia returnează
- 1, dacă ştergerea a fost posibilă;
- 0, în caz contrar.
Funcţia şterge logic articolul curent din fişierul de date. Ştergerea se
realizează fizic în tabela de indexuri. Iniţial, indicatorul de stare este setat pe 0 şi
apoi se elimină articolul din tabelă, prin apelul funcţiei Sort. Funcţia returnează
valoarea 0, corespunzătoare situaţiei de eroare, dacă pointerul curent al tabelei de
indexuri indică marcatorul de sfârşit de fişier.

¾ Funcţia pentru ştergerea în acces direct a unui articol, cu prototipul


int DeleteKey(char *Key);
Funcţia primeşte ca parametru de intrare cheia articolului care va fi şters şi
returnează
- 1, dacă ştergerea a fost posibilă;
- 0, în caz contrar.
Funcţia şterge logic din fişierul de date articolul a cărui cheie este primită
ca parametru. Ştergerea este realizată fizic din tabela de indexuri, analog ştergerii
în acces secvenţial. Funcţia returnează valoarea 0, corespunzătoare situaţiei de
eroare, dacă Key nu este regăsită în tabela de indexuri. Căutarea este realizată prin
apelul funcţiei SeekKey.
Pentru implementarea funcţiilor descrise mai sus, sunt utilizate următoarele
funcţii auxiliare:
¾ Funcţia pentru sortarea tabelei de indexuri, cu eliminarea articolelor cu
stare 0, cu prototipul
void Sort();
Funcţia realizează sortarea articolelor tabelei de indexuri, crescător după
câmpul cheie, precum şi ştergerea fizică a tuturor articolelor cu indicator de stare 0.

95
Programarea calculatoarelor

¾ Funcţia pentru căutarea articolului cu cheie dată, cu prototipul


int SeekKey(char *Key)
Funcţia primeşte ca parametru de intrare cheia articolului căutat şi
returnează
- 1, dacă articolul a fost găsit;
- 0, în caz contrar.
Funcţia realizează căutarea binară în tabela de indexuri, după câmpul
cheie. Dacă articolul cu cheia Key este găsit, funcţia lasă pointerul de fişier pe acel
articol (o citire secvenţială ulterioară determinând obţinerea articolului
corespunzător din fişierul de date).

Textul sursă care implementează toate aceste funcţii C este prezentat în


continuare (în exemplele care urmează, aceste text va fi considerat salvat în fişierul
index1.cpp).
#include <stdio.h>
#include <string.h>

#define fisier FILE*


typedef struct{ char cheie[7];
char den[35];
float pu;
float cant;
} ARTICOL; //tipul articol din fisierul de date

typedef struct{ char is;


char cheie[7];
long nr_rel;
} ART_INDEX; //tipul articol din tabela index

fisier ind; //fisierul index


char nume_index[20]; //numele extern al fisierului index

void Sort()
{ ART_INDEX a,b;
fisier ind1;
long i,j;
ind1=fopen("temp.idx","wb+");
rewind(ind);
fread(&a,sizeof(a),1,ind);
while(!feof(ind))
{ if(a.is)fwrite(&a,sizeof(a),1,ind1);
fread(&a,sizeof(a),1,ind);
}
fclose(ind);
fseek(ind1,0,SEEK_END);
long n=ftell(ind1)/sizeof(a);
for(i=0;i<n-1;i++)
{ fseek(ind1,i*sizeof(a),SEEK_SET);
fread(&a,sizeof(a),1,ind1);
for(j=i+1;j<n;j++)
{ fseek(ind1,j*sizeof(a),SEEK_SET);
fread(&b,sizeof(a),1,ind1);
if(strcmp(a.cheie,b.cheie)>0)
{ fseek(ind1,i*sizeof(a),SEEK_SET);
fwrite(&b,sizeof(a),1,ind1);
fseek(ind1,j*sizeof(a),SEEK_SET);

96
Algoritmi de prelucrare a fişierelor binare

fwrite(&a,sizeof(a),1,ind1);
}
}
}
rewind(ind1);
ind=fopen(nume_index,"wb+");
fread(&a,sizeof(a),1,ind1);
while(!feof(ind1))
{ if(a.is)fwrite(&a,sizeof(a),1,ind);
fread(&a,sizeof(a),1,ind1);
}
fclose(ind1);
remove("temp.idx");
}
/* cautarea articolului cu cheia Key si plasarea pointerului de
fisier in tabela de indexuri pe articolul respectiv*/
int SeekKey(char *Key)
{ long ls=0, ld, m, n;
ART_INDEX a;
int gasit=0;
fseek(ind,0,SEEK_END);
n=ftell(ind)/sizeof(ART_INDEX);
ld=n-1;
while((ls<=ld)&&(!gasit))
{ m=(ls+ld)/2;
fseek(ind,m*sizeof(a),SEEK_SET);
fread(&a,sizeof(a),1,ind);
if(strcmp(a.cheie,Key)==0) gasit=1;
else if(strcmp(a.cheie,Key)>0) ld=m-1;
else ls=m+1;
}
if(gasit) fseek(ind,m*sizeof(a),SEEK_SET);
return gasit;
}
void new_index(char *nume)
{ strcpy(nume_index,nume);
strcat(nume_index,".idx");
ind=fopen(nume_index,"wb+");
}
void open_index(char *nume)
{ strcpy(nume_index,nume);
strcat(nume_index,".idx");
ind=fopen(nume_index,"rb+");
}
void close_index()
{ fclose(ind);
}
int ReadSec(fisier f,ARTICOL *a)
{ ART_INDEX a1;
int r;
fread(&a1,sizeof(a1),1,ind);
if(feof(ind))r=0;
else { fseek(f,a1.nr_rel*sizeof(*a),SEEK_SET);
fread(a,sizeof(*a),1,f);
r=1;
}
return r;
}

97
Programarea calculatoarelor

int ReadKey(fisier f,ARTICOL *a,char *Key)


{ ART_INDEX a1;
int r;
if(SeekKey(Key))
{ fread(&a1,sizeof(a1),1,ind);
fseek(f,a1.nr_rel*sizeof(*a),SEEK_SET);
fread(a,sizeof(*a),1,f);
r=1;
}
else r=0;
return r;
}
int WriteSec(fisier f,ARTICOL a)
{ ART_INDEX a1, ai;
long n, nl;
int r;
fseek(ind,0,SEEK_END);
n=ftell(ind)/sizeof(a1);
if(n>0)
{ fseek(ind,(n-1)*sizeof(a1),SEEK_SET);
fread(&a1,sizeof(a1),1,ind);
if(strcmp(a1.cheie,a.cheie)>0) r=0;
else { ai.is=1;
strcpy(ai.cheie,a.cheie);
fseek(f,0,SEEK_END);
n1=ftell(f)/sizeof(a);
ai.nr_rel=n1;
fseek(ind,0,SEEK_END);
fwrite(&ai,sizeof(ai),1,ind);
fwrite(&a,sizeof(a),1,f);
r=1;
}
}
else r=0;
return r;
}
int WriteKey(fisier f,ARTICOL a)
{ char Key[7];
ART_INDEX a1;
long n;
strcpy(Key,a.cheie);
if(SeekKey(Key)) r=0;
else { a1.is=1;
strcpy(a1.cheie,a.cheie);
fseek(f,0,SEEK_END);
n=ftell(f)/sizeof(a);
a1.nr_rel=n;
fwrite(&a,sizeof(a),1,f);
fseek(ind,0,SEEK_END);
fwrite(&a1,sizeof(a1),1,ind);
Sort();
r=1;
}
return r;
}

98
Algoritmi de prelucrare a fişierelor binare

int DeleteSec()
{ ART_INDEX a1;
long pos=ftell(ind);
fread(&a1,sizeof(a1),1,ind);
if(feof(ind)) r=0;
else { fseek(ind,pos,SEEK_SET);
a1.is=0;
fwrite(&a1,sizeof(a1),1,ind);
Sort();
r=1;
}
return r;
}

int DeleteKey(char *Key)


{ int r;
if(SeekKey(Key))
r=DeleteSec();
else r=0;
return r;
}

Exemple
10. Scrieţi programul C pentru crearea în acces direct unui fişier binar cu articole
având structura:
cheie denumire preţ cantitate
Datele sunt preluate de la tastatură până la apăsarea combinaţiei CTRL/Z pentru
câmpul cheie. Fişierul creat este organizat indexat.
#include "index1.cpp"
#include <conio.h>

void main()
{ ARTICOL a;
char nume[20],nume1[20];
char x[7];
fisier f;
clrscr();
printf(" numele fisierului de date in care adaugati:");
fflush(stdin);
gets(nume);
strcpy(nume1,nume);
strcat(nume1,".dat");
f=fopen(nume1,"rb+");
if(f==NULL)
{ printf("Fisierul va fi creat");
f=fopen(nume1,"wb+");
new_index(nume);
}
else open_index(nume);
printf("\nAdaugarea in acces direct dupa cheie\n");
printf("Introduceti cheia:");
fflush(stdin);
gets(a.cheie);
while(!feof(stdin))
{ printf("Denumire produs:");
fflush(stdin);
gets(a.den);
printf("Pret produs:");
scanf("%f",&a.pu);

99
Programarea calculatoarelor

printf("Cantitate:");
scanf("%f",&a.cant);
if(WriteKey(f,a))
printf("Articol adaugat");
else printf("Exista articol");
getch();
clrscr();
printf("Introduceti cheia:");
fflush(stdin);
gets(a.cheie);
}
fclose(f);
close_index();
getch();
}

11. Se presupune creat şi populat fişierul de date din exemplul anterior. Scrieţi
programul C pentru ştergerea acelor articole ale căror chei sunt introduse de la
tastatură. Încheierea introducerii datelor este marcată standard.
#include "index1.cpp"
#include <conio.h>

void main()
{ ARTICOL a;
char nume[20],nume1[20];
char Key[7];
fisier f;
char r;
int i;
clrscr();
printf(" numele fisierului de date din care stergeti:");
fflush(stdin);
gets(nume);
strcpy(nume1,nume);
strcat(nume1,".dat");
f=fopen(nume1,"rb+");
if(f==NULL)
printf("Fisierul nu exista!!");
else
{ open_index(nume);
printf("\nStergerea in acces direct dupa cheie\n");
printf("Introduceti cheia:");
fflush(stdin);
gets(Key);
while(!feof(stdin))
{ if(ReadKey(f,&a,Key))
{ printf("Articolul:\n");
printf("Denumire:%20s\n",a.den);
printf("Pret:%7.2f\n",a.pu);
printf("Cantitate:%8.2f\n\n",a.cant);
printf("Doriti stergerea?(D/Altceva)");
r=getch();
if(r=='D')
{ i=DeleteKey(Key);
printf("Stergere efectuata");
}
else printf("Stergerea nu a fost efectuata");
}
else printf("Nu exista articol");
getch();

100
Algoritmi de prelucrare a fişierelor binare

clrscr();
printf("Introduceti cheia:");
fflush(stdin);
gets(a.cheie);
}
fclose(f);
close_index();
getch();
}
}

12. Scrieţi programul C pentru afişarea informaţiilor memorate în fişierul creat în


exemplul 10. Articolele sunt afişate în ordine crescătoare a câmpului cheie.
#include "index1.cpp"
#include <conio.h>
void main()
{ ARTICOL a;
char nume[20],nume1[20];
char x[7];
fisier f;
clrscr();
printf(" numele fisierului de date care este consultat:");
fflush(stdin);
gets(nume);
strcpy(nume1,nume);
strcat(nume1,".dat");
f=fopen(nume1,"rb+");
if(f==NULL) printf("Fisierul nu exista!!");
else { open_index(nume);
while(ReadSec(f,&a))
{ printf("Cheie:");
puts(a.cheie);
printf("\nDenumire produs:");
puts(a.den);
printf("Pret produs:");
printf("7.2%f\n",a.pu);
printf("Cantitate:");
printf("%8.2f\n\n",a.cant);
getch();
}
fclose(f);
close_index();
getch();
}
}

6.4 Sortarea fişierelor binare memorate dens


Operaţia de sortare a unui fişier binar presupune aranjarea articolelor în
ordinea crescătoare (descrescătoare) a valorilor unei zone, numită cheie de sortare. În
cazul în care cheia de sortare este formată dintr-un singur câmp din cadrul articolului,
operaţia se numeşte sortare simplă. Sortarea multiplă presupune aranjarea articolelor
după valorile a două sau mai multe câmpuri, alcătuind, prin juxtapunere, cheia de
sortare. Juxtapunerea câmpurilor (nu neapărat adiacente în cadrul articolului) se
realizează pe lungimea efectivă a lor, alcătuind forma canonică a cheii de sortare. De

101
Programarea calculatoarelor

exemplu, dacă NUME şi PRENUME sunt două câmpuri distincte, declarate de tip şir
de caractere, forma canonică a cheii de sortare după nume şi prenume este dată de
lungimea efectivă a fiecărei date de tip şir.
Dacă pentru sortarea simplă cheia de sortare poate fi însuşi câmpul din
articol, pentru cea multiplă este necesară o zonă auxiliară de memorie, în care se
construieşte cheia de sortare, în forma canonică.
Sortarea unui fişier se poate realiza cu aducerea lui integrală în memorie
(sortare în memorie) sau cu aducerea în memorie a câte unui articol (sortare "direct
pe disc"). Indiferent de modul utilizat, sortarea poate fi realizată printr-unul din
algoritmii cunoscuţi pentru masivele de date: sortare prin interschimbare, prin
selecţie, prin inserţie etc.

Sortarea în memorie este o metodă rapidă şi presupune: citirea întregului


fişier în memoria principală, într-o structură internă de date (vector, arbore bina de
sortare); sortarea efectivă după cheia de sortare (în cazul folosirii unui vector,
operaţia nu este necesară dacă se foloseşte arbore de sortare); recrearea fişierului pe
disc. Metoda se poate aplica numai fişierelor reduse ca dimensiuni sau cu lungime
mică de articol, dată fiind capacitatea limitată a memoriei interne asociată unui
program. Ea poate avea mai multe variante:
Sortarea cu vehicularea întregului articol, presupune memorarea întregului
fişier într-un vector de articole. Compararea pentru sortare se va realiza pe câmpul
cheie de sortare, însă interschimbarea se realizează la nivelul întregului articol.

Exemplu:
13.
typedef struct { char grupa;
char nume_student[30];
float medie;
}STUDENT;
void main()
{ FILE* f;
STUDENT x[250], aux;
int i,j,n;
f=fopen("STUDENT.DAT","rb+");
fseek(f,0,SEEK_END);
n:=ftell(f)/sizeof(STUDENT);
rewind(f);
// citirea fisierului initial in memorie
for(i=0;i<n;i++)
fread(&x[i],sizeof(STUDENT),1,f);
//sortarea
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if(x[i].medie>x[j].medie)
{ aux:=x[i]; //interschimbarea articolelor
x[i]:=x[j]; //nu se poate folosi atribuirea mereu
x[y]:=aux;
}
rewind(f);

102
Algoritmi de prelucrare a fişierelor binare

for(i=0;i<n;i++)
fwrite(&x[i],sizeof(STUDENT),1,f);
fclose (f); }

Sortarea cu vehicularea cheii şi indexului, presupune memorarea într-un


vector numai a valorii cheii de sortare, împreună cu numărul relativ al articolului din
fişierul iniţial (indexul). Interschimbarea se va realiza la nivelul cheii de sortare,
rezultând în final ordinea în care articolele vor fi scrise în fişier. Deoarece articolele,
în întregime, sunt rezidente pe disc, fişierul sortat va fi creat cu un alt nume fizic, în
acces secvenţial, preluînd articolele din fişierul iniţial, în acces direct. Cheile de
sortare şi indexurile pot fi memorate în vectori distincţi sau într-unul singur, cu
elemente de tip articol.

Exemplu:
14.
typedef struct { char grupa;
char nume_student[30];
float medie;
}STUDENT;
typedef struct { float medie;
int index;
}CHEIE;
void main()
{ FILE *f,*g;
STUDENT y;
CHEIE aux,x[250];
int i,j,n;
f=fopen("STUDENT.DAT","rb");
fseek(f,0,SEEK_END);
n:=ftell(f)/sizeof(STUDENT);
rewind(f);
g=fopen("STUDENTS.DAT","wb");
// ----------------------------------
for(i=0;i<n;i++)
{ fread(&y,sizeof(STUDENT),1,f);
x[i].medie=y.medie;
x[i].index=i;
}
//-------------------------------------
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if(x[i].medie>x[j].medie)
{ aux=x[i];
x[i]:=x[j];
x[j]:=aux
}
//--------------------------------------
for(i=0;i<n;i++)
{ fseek(f,x[i].index*sizeof(STUDENT),SEEK_SET);
fread(&y,sizeof(STUDENT),1,f);
fwrite(&y,sizeof(STUDENT),1,g);
}
fclose(f);
fclose(g);
unlink(f);
rename("STUDENTS.DAT","STUDENT.DAT");
}

103
Programarea calculatoarelor

Sortarea numai cu vehicularea indexului este o metodă mai bună decât


precedenta, deoarece micşorează timpul de execuţie, prin eliminarea interschimbării
valorilor cheii de sortare (mai ales când aceasta este multiplă). Valorile cheii de
sortare şi numărului relativ corespunzător indexului se memorează în vectori distincţi.
Comparaţiile se realizează pentru valorile cheii, dar interschimbarea se efectuează
numai pentru indexuri. Se va crea un alt fişier fizic.

Exemplu:
15.
typedef struct { char grupa;
char nume_student[30];
float medie;
}STUDENT;
void main()
{ FILE *f,*g;
float x[250];
int index[250];
int i,j,n;
STUDENT y;
f=fopen("STUDENT.DAT","rb");
fseek(f,0,SEEK_END);
n:=ftell(f)/sizeof(STUDENT);
rewind(f);
g=fopen("STUDENTS.DAT","wb");
//------------------------------------------
for(i=0;i<n;i++)
{ fread(&y,sizeof(STUDENT),1,f);
x[i]=y.medie;
index[i]=i;
}
//------------------------------------------
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if(x[i]>x[j])
{ aux=index[i];
index[i]=index[j];
index[j]=aux
}
//-------------------------------------------
for(i=0;i<n;i++)
{ fseek(f,index[i]*sizeof(STUDENT),SEEK_SET);
fread(&y,sizeof(STUDENT),1,f);
fwrite(&y,sizeof(STUDENT),1,g);
}
fclose(f);
fclose(g)
}

Sortarea pe disc se aplică fişierelor mari, la care este imposibilă aducerea în


memoria principală chiar şi a minimului de informaţii necesare sortării. În acest caz,
operaţia se va realiza "direct" pe mediul magnetic, cu aducerea în memorie doar a
două articole (pentru comparaţii) şi scrierea în acces direct în acelaşi fişier, prin
utilizarea numărului relativ al articolelor prelucrate. Timpul de prelucrare va fi
substanţial mai mare decât la metodele de sortare în memorie, deoarece operaţiile de
intrare/ieşire sunt costisitoare din punct de vedere al resursei timp calculator.

104
Algoritmi de prelucrare a fişierelor binare

Se poate aplica oricare din algoritmii de sortare cunoscuţi, cu menţiunea că indicii i şi


j vor fi utilizaţi pentru controlul numărului relativ al articolelor în fişier.

Exemple:
16. Sortarea prin interschimbare
do
{ vb=0;
for(i=0;i<nr_art-1;i++)
{ fseek(f,sizeof(STUDENT)*i,SEEK_SET);
fread(&x,sizeof(STUDENT),1,f);
fread(&y,sizeof(STUDENT),1,f);
if(x.medie>y.medie)
{ fseek(f,sizeof(STUDENT)*i,SEEK_SET);
fwrite(&y,sizeof(STUDENT),1,f);
fwrite(&x,sizeof(STUDENT),1,f);
vb=1;
}
while(vb);

17. Sortare prin selecţie


for(i=0;i<nr_art-1;i++)
{ fseek(f,sizeof(STUDENT)*i,SEEK_SET);
fread(&x,sizeof(STUDENT),1,f);
for(j=i+1;j<nr_art;j++)
{ fseek(f,sizeof(STUDENT)*j,SEEK_SET);
fread(&y,sizeof(STUDENT),1,f);
if(x.medie>y.medie)
{ fseek(f,sizeof(STUDENT)*i,SEEK_SET);
fwrite(&y,sizeof(STUDENT),1,f);
fseek(f,sizeof(STUDENT)*j,SEEK_SET);
fwrite(&x,sizeof(STUDENT),1,f);
}
}
}

6.5 Interclasarea fişierelor binare memorate dens


Interclasarea este operaţia prin care, din două sau mai multe mulţimi
ordonate, se obţine o nouă mulţime, ordonată după acelaşi criteriu. Interclasarea
fişierelor apare ca necesitate în aplicaţiile economice, mai ales în faza de post-
populare a fişierelor mari de date, create simultan pe submulţimi de mai mulţi
utilizatori şi necesitând, în final, reunirea acestora într-unul singur.
Condiţia apriori interclasării este ca toate fişierele parţiale să fie sortate după
valorile aceluiaşi câmp, pe baza căruia se va realiza, prin comparări succesive,
operaţia de interclasare. Câmpul poartă denumirea de cheie de interclasare.
Interclasarea a n fişiere se poate realiza simplu prin aplicarea de n-1 ori a operaţiei de
interclasare a două fişiere (figura 6.12).

105
Programarea calculatoarelor

...
1 2 3 4 n

Interclasare 1 ………..

Fişier 1

Interclasare 2 ………………………...

Fişier 2

Interclasare 3 …………………………………………...

Fişier 3

...
Interclasare 3 ………………………………………………………………………………………………………….

Fişier
final Fişier n-1

Figura 6.13 Interclasarea a n fişiere

Se obţin astfel n-1 fişiere intermediare (fişier i), din care numai ultimul se
păstrează, celelalte (împreună cu fişierele iniţiale) se şterg, fie în finalul procesului,
fie la sfârşitul fiecărei etape intermediare (recomandat). Interclasarea a două fişiere
este similară operaţiei aplicate pentru doi vectori. Dimensiunea fişierului rezultat este
suma dimensiunilor fişierelor iniţiale.

Exemplu:
18. Se prezintă structura principială a unui program pentru interclasarea a două fişiere
binare. Cheile de interclasare se află în câmpul c aparţinând articolelor art_1 şi art_2,
corespunzătoare fişierelor de intrare f şi g, considerate populate dens.
{
//---------------------------------
//citire nume externe ale fisierelor
//---------------------------------
f=fopen(nume_fisier_intrare_1, "rb");
g=fopen(nume_fisier_intrare_2, "rb");
h=fopen(nume_fisier_iesire, "wb");
fread(&art_1,sizeof(tip_articol),1,f);
fread(&art_2,sizeof(tip_articol),1,g);
while((!feof(f)&&(!feof(g)))
if(art_1.c>art_2.c)
{ fwrite(&art_1,sizeof(tip_articol),1,h);
fread(&art_1,sizeof(tip_articol),1,f);
}

106
Algoritmi de prelucrare a fişierelor binare

else
{ fwrite(&art_2,sizeof(tip_articol),1,h);
fread(&art_2,sizeof(tip_articol),1,g);
}
while(!feof(f))
{ fwrite(&art_1,sizeof(tip_articol),1,h);
fread(&art_1,sizeof(tip_articol),1,f);
}
while(!feof(g))
{ fwrite(&art_2,sizeof(tip_articol),1,h);
fread(&art_2,sizeof(tip_articol),1,g);
}
fclose(f);
fclose(g);
fclose(h)
}

6.6 Prelucrarea masivelor memorate în fişiere binare


Una dintre aplicaţiile des întâlnite în lucrul cu fişiere este memorarea
masivelor de date de dimensiuni foarte mari, care fac imposibilă aducerea lor
integrală în memoria internă. Problema principală a prelucrării masivelor (vectori,
matrice etc.) memorate în fişiere binare, o constituie determinarea poziţiei unui
anumit element de masiv în cadrul fişierului. Indiferent de numărul de dimensiuni ale
masivului şi de modalităţile de memorare a elementelor sale în cadrul fişierului,
legătura între elementul de masiv care se referă şi numărul relativ al articolului care îl
conţine se realizează pe baza funcţiei rang.
În cazul masivelor memorate în fişiere, prelucrarea acestora depinde de unele
caracteristici particulare:
numărul de dimensiuni ale masivului;
ordinea de memorare în fişier (în ordine lexicografică sau invers lexicografi-
că);
modul de memorare (dens sau nedens);
ordinea de parcurgere a masivului.

6.6.1 Prelucrarea vectorilor

De regulă, vectorii se memorează dens. Numărul relativ al articolului depinde


de rangul elementului în cadrul vectorului, astfel:
nr_relativ = rang(xi)+1 = i+1, pentru i=0..n-1, dacă articolul cu numărul
relativ 0, fie nu este utilizat (caz în care dimensiunea vectorului este n = dimensiune
fişier-1), fie memorează numărul efectiv de componente ale vectorului;
nr_relativ = rang(xi) = i, pentru i=0..n, dacă vectorul se memorează începând
cu primul articol (caz în care dimensiunea vectorului este n =dimensiunea fişierului).
Exemplu:
19. Să se determine media aritmetică a elementelor unui vector foarte mare, memorat
într-un fişier binar.

107
Programarea calculatoarelor

#include<stdio.h>
void main()
{ FILE* vector;
float element, medie;
long i,n;
vector=fopen("VECTOR.DAT","rb");
fseek(vector,0,SEEK_END);
n=ftell(f)/sizeof(float);
rewind(f);
medie=0;
for(i=0;i<n;i++)
{ fread(&element,sizeof(float),1,vector);
medie+=element;
}
medie/=n;
printf("\nMedia: %7.3f",medie);
fclose(vector); }

6.6.2 Prelucrarea matricelor

O matrice poate fi memorată într-un fişier binar nedens (similar memorării în


MP) sau dens, în ordine lexicografică sau invers lexicografică. Numărul relativ al
elementului aij se determină pe baza funcţiei rang, astfel:
rang(aij) = i * nr_coloane + j, în cazul memorării lexicografice, unde
nr_coloane este fie numărul coloanelor efective (populare densă), fie numărul
coloanelor rezervate (populare nedensă);
rang(aij) = j * nr_linii + i, în cazul memorării invers lexicografice, unde
nr_linii este fie numărul liniilor efective (populare densă), fie numărul liniilor rezer-
vate (populare nedensă).
Fie m şi n numărul liniilor, respectiv coloanelor efective şi mr şi nr numărul
liniilor, respectiv coloanelor rezervate (mr şi nr corespund dimensiunilor maxime din
declaraţia unui masiv aflat în memoria principală). Pentru ca fişierul să conţină
informaţii complete despre matrice, trebuie să memoreze, pe lângă elementele ei, şi:
m (sau n), în cazul memorării dense. Când se memorează m, n se determină
împărţind dimensiunea fişierului (mai puţin primul articol, unde se află m) la m; când
se memorează n, m se determină împărţind dimensiunea fişierului (mai puţin primul
articol, unde se află n) la n. Funcţia rang depinde de m sau n, după cum matricea este
memorată invers lexicografic sau lexicografic;
n şi nr, în cazul memorării nedense în ordine lexicografică. m se determină
împărţind dimensiunea fişierului (mai puţin primele două articole, unde se află n şi
nr) la nr, iar mr nu are relevanţă. Funcţia rang depinde de nr;
m şi mr, în cazul memorării nedense în ordine invers lexicografică. N se
determină împărţind dimensiunea fişierului (mai puţin primele două articole, unde se
află m şi mr) la mr, iar nr nu are relevanţă. Funcţia rang depinde de mr.
Funcţia rang se calculează şi se utilizează numai dacă problema de rezolvat
implică parcurgerea matricei în altă ordine decât cea în care este memorată în fişier,
deci consultarea acestuia se realizează în acces direct.

108
Algoritmi de prelucrare a fişierelor binare

Exemple:
20. Să se afişeze elementul maxim de pe fiecare coloană a unei matrice de dimensiuni
mxn, memorate dens, într-un fişier binar, în ordine lexicografică. Primul articol
conţine numărul de coloane.
• Observaţie: primul articol are dimensiune diferită de celelalte: numărul de
coloane este de tip întreg iar elementele matricei sunt reale. Din dimensiunea
totală a fişierului, primii sizeof(int) octeţi sunt ocupaţi de numărul de
coloane, restul constituie matricea propriu-zisă. La calcularea poziţiei unui
element în matrice trebuie ţinut cont de faptul că matricea nu începe la
începutul fişierului, ci după sizeof(int) octeţi.
#include<stdio.h>
void main()
{ FILE *f;
float max, element;
long i,j,r,m,n;
f=fopen("MATRICE.DAT", "rb");
fread(&n,sizeof(int),1,f); //citire numar de coloane
fseek(f,0,SEEK_END);
m=(ftell(f)-sizeof(int))/(sizeof(float)*n);
for(j=0;j<n;j++)
{ //pozitonare pe primul element din coloana j
fseek(f,j*sizeof(float)+sizeof(int),SEEK_SET);
fread(&element,sizeof(float),1,f);
max=element;
for(i=1;i<m;i++)
{ r=i*n+j; //rangul elementului in matrice
fseek(f,r*sizeof(float)+sizeof(int),SEEK_SET);
fread(&element,sizeof(float),1,f);
if(element>max) max=element;
}
printf("\Maximul pe coloana %2d este %7.3f",j,max);
}
fclose(f);
}

21. Să se determine elementul maxim de pe fiecare coloană a unei matrice de


dimensiuni m x n, memorată nedens într-un fişier binar, în ordine lexicografică.
Primele două articole conţin numărul de coloane efective şi, respectiv, numărul
rezervat de coloane.
Rezolvarea este similară cu exemplul anterior, cu următoarele modificări:
• din dimensiunea totală a fişierului, primii 2*sizeof(int) octeţi sunt ocupaţi
de dimensiunile matricei (număr de coloane efectiv respectiv rezervat), iar
restul constituie matricea propriu zisă. La calcularea poziţiei unui element
în matrice trebuie ţinut cont de faptul că matricea nu începe la începutul
fişierului, ci după 2*sizeof(int) octeţi.
• La calcularea rangului unui element (şi implicit a poziţiei sale în fişier) se
foloseşte numărul de coloane rezervare, nu numărul de coloane efective.

109
7 Structuri dinamice de date. Liste

Organizarea de tip listă corespunde unei structurări lineare a datelor, în


sensul că la nivelul fiecărei componente există suficientă informaţie pentru
identificarea următoarei componente a colecţiei. Datele unei mulţimi structurate
prin intermediul listelor sunt referite de obicei prin termenii de noduri, celule,
componente etc.

7.1 Reprezentarea listelor


Reprezentarea unei liste poate fi realizată static prin intermediul structurii de
date vector. În acest caz ordinea componentelor este dată de ordinea pe domeniul de
valori corespunzător 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 C utilizând un masiv unidimensional.
Principalele dezavantaje ale utilizării reprezentării statice rezidă din
volumul de calcule necesare efectuării operaţiilor de inserare/eliminare de noduri şi
din necesitatea păstrării unei zone de memorie alocată, indiferent de lungimea
efectivă a listei.
Aceste dezavantaje pot fi eliminate prin opţiunea de utilizare a structurilor
dinamice. Componentele unei liste dinamice sunt omogene, de tip articol. Fiecare
nod, considerat separat, este o structură eterogenă, 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 NULL în cazul listelor „deschise” (lineare)

110
Structuri dinamice de date. Liste

şi respectiv indică adresa primei componente din listă în cazul listelor „închise”
(circulare).
Declararea tipurilor de date C pentru definirea structurilor de liste dinamice
simplu şi respectiv dublu înlănţuite este:
a) Listă simplu înlănţuită b) Listă dublu înlănţuită

typedef struct nod{ typedef struct nod{


tip_informatie inf; tip_informatie inf;
struct nod *leg; struct nod *ls, *ld;
} list, *lista; } list, *lista;

unde tip_informatie este numele tipului de date C utilizat pentru memorarea


fiecărei date din mulţimea{d1, d2,…, dn}.
În cele ce urmează vom considera că tip_informatie este tipul C int.

7.2 Operaţii primitive asupra listelor


Accesul la informaţia stocată într-o variabilă de tip listă revine la
efectuarea următoarelor operaţii 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ă (modificată).
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. În continuare ne vom referi exclusiv la liste dinamice, studiul
listelor reprezentate prin intermediul vectorilor fiind propus cititorului.
1. Parcurgerea datelor memorate într-o listă
Funcţia C parc implementează parcurgerea unei liste dinamice în varianta
simplu înlănţuită şi în cazul listelor dublu înlănţuite. Se presupune că declaraţiile
de tip pentru definirea structurilor de liste menţionate anterior sunt globale, relativ
la procedurile descrise în continuare.
a) Lista reprezentată prin structură dinamică simplu înlănţuită
void parc(lista cap)
{ if(cap)
{ printf("%i ",cap->inf);
parc(cap->leg);
}
}

111
Programarea calculatoarelor

b) Lista reprezentată prin structură dinamică dublu înlănţuită

void parc(lista cap)


{ if(cap)
{ printf("%i ",cap->inf);
parc(cap->ls);
}
}

2. Regăsirea unei date într-o colecţie memorată într-o listă


Funcţia C cauta calculează adresa nodului în care este găsit elementul
căutat. Dacă valoarea căutată nu se regăseşte printre elementele listei, funcţia
returnează valoarea NULL.
a) Lista reprezentată prin structură dinamică simplu înlănţuită
lista cauta(lista cap,int info)
{ if(cap==NULL)return NULL;
else if(cap->inf==info) return cap;
else return cauta(cap->leg,info);
}

b) Lista reprezentată prin structură dinamică dublu înlănţuită


lista cauta(lista cap,int info)
{ if(cap==NULL)return NULL;
else if(cap->inf==info) return cap;
else return cauta(cap->ls,info);
}

3. Inserarea unei date î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.
Deoarece prin inserarea unei componente se poate ajunge la depăşirea
spaţiului disponibil de memorie, este necesară verificarea în prealabil dacă este
posibilă inserarea sau nu (dacă se poate aloca spaţiu de memorie pentru
componenta de inserat). În continuare ne vom referi la liste dinamice simplu
înlănţuite. Operaţiile de inserare în cazul listelor dinamice dublu înlănţuite pot fi
realizate similar cazului listelor simplu înlănţuite, cu specificarea ambelor câmpuri
de adresă ale nodurilor.
Pentru exemplificarea operaţiei de inserare sunt prezentate funcţiile de
inserare la începutul listei, inserare după ultimul element al listei şi inserarea unei
celule după un nod cu informaţie dată.

Inserarea la începutul listei


Funcţia C inserare_la_inceput returnează valoarea 1 dacă adăugarea unui
nou element este posibilă (spaţiul de memorie este suficient pentru o nouă alocare),
altfel returnează 0. În cazul în care inserarea este posibilă, prin apelul funcţii este
realizată adăugarea unui nou nod la începutul listei.

112
Structuri dinamice de date. Liste

int inserare_la_inceput(lista *cap,int info)


{ lista nou;
if(nou=(lista)malloc(sizeof(list)))
{ nou->inf=info;
nou->leg=*cap;
*cap=nou;
return 1;
}
return 0;
}

Inserarea după ultima componentă a unei liste


Funcţia C inserare_la_sfarsit returnează 1 dacă şi numai dacă este posibilă
o inserare, altfel calculează 0. Pentru inserarea unui nou nod în listă după ultima
celulă este necesar calculul ultimului nod al listei, notat p.

int inserare_la_sfarsit(lista *cap,int info)


{ lista nou;
if(nou=(lista)malloc(sizeof(list)))
{ nou->leg=NULL;nou->inf=info;
if(cap==NULL)*cap=nou;
else
{ for(lista p=*cap;p->leg;p=p->leg);
p->leg=nou;
}
return 1;
}
return 0;
}

Inserarea unei informaţii după o celulă cu informaţie cunoscută


Inserarea unui nou nod într-o listă identificată prin variabila cap după o
celulă p cu informaţie cunoscută, infod, poate fi realizată astfel. Este apelată funcţia
de căutare cauta, care calculează nodul p cu proprietatea că informaţia memorată în
p este infodat. Dacă p este adresa vidă sau dacă spaţiul de memorie disponibil nu
este suficient, inserarea nu poate fi realizată. În caz contrar, este inserat un nou nod
între celulele p şi p->leg.

int inserare_dupa_informatie(lista cap,int info,int infod)


{
lista nou,p;
if(nou=(lista)malloc(sizeof(list)))
if(p=cauta(cap,infod)){
nou->inf=info;
nou->leg=p->leg;
p->leg=nou;
return 1;
}
return 0;
}

113
Programarea calculatoarelor

4. Eliminarea unei date 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 care precede/urmează primei componente 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.
Informaţia i ataşată nodului eliminat din listă reprezintă dată de ieşire
pentru orice modul de eliminare, în cazul în care i nu este cunoscută înaintea
eliminării (de exemplu, atunci când este solicitată eliminarea unei celule care
conţine o informaţie dată) .
Eliminarea unui nod p poate fi realizată logic sau fizic. Eliminarea logică a
celulei p este efectuată excluzând p din lista dinamică prin setarea legăturii nodului
care precede p pe adresa succesorului lui p, dacă p nu este adresa primului element
al listei, cap, respectiv prin atribuirea adresei primului element al listei cu
cap->leg, în caz contrar.
Eliminarea cu ştergere fizică unui nod p presupune redenumirea acelui nod
în scopul eliberării memoriei ocupate de p şi efectuarea operaţiilor descrise în
cadrul procesului de eliminare logică.
În continuare sunt prezentate următoarele tipuri de eliminări, cu ştergere
fizică.

Eliminarea primei componente a unei liste


Funcţia C elimina_de_la_inceput returnează 1 dacă lista nu este vidă, deci
eliminarea primului nod este posibilă, altfel returnează 0. Dacă lista conţine măcar
un nod, este eliminată prima celulă.

Int elimina_de_la_inceput(lista *cap,int *info)


{ if(*cap)
{ lista aux=*cap;
*info=aux->inf;
*cap=(*cap)->leg;
free(aux);
return 1;
}
return 0;
}

Eliminarea ultimei componente a unei liste


Similar operaţiei de inserare a unui nod după ultima celulă a unei liste,
eliminarea ultimului nod presupune determinarea acelei celule p cu proprietatea că
p->leg este NULL. Funcţia C elimina_ultim returnează 1 dacă lista nu este vidă,

114
Structuri dinamice de date. Liste

caz în care este eliminat cu ştergere ultimul nod al listei. Dacă lista este vidă,
funcţia calculează valoarea 0.
Int elimina_ultim(lista *cap,int *info)
{ if (*cap)
{ if((*cap)->leg)
{ for(lista p=*cap;p->leg->leg;p=p->leg);
*info=p->leg->inf;
free(p->leg);
p->leg=NULL;
}
else
{ *info=(*cap)->inf;
free(*cap);
*cap=NULL;
}
return 1;
}
return 0;
}

Eliminarea primei celule a unei liste care are informaţia egală


cu o informaţie dată
Pentru realizarea acestei operaţii se poate proceda astfel. Sunt calculate aux
şi p, unde aux este nodul care precede celulei cu informaţie dată în lista din care
este efectuată eliminarea (funcţia C cautaprecedent) şi p=aux->leg.. Dacă p este
NULL, atunci eliminarea este imposibilă. Dacă aux este NULL, atunci eliminarea
revine la extragerea cu ştergere a primului nod din listă, altfel este eliminată celula
p, succesoare a lui aux în listă. Funcţia C elimina_informatie implementează
operaţia de eliminare a unui nod cu informaţie dată, info, din lista identificată prin
parametrul cap.
lista cautaprecedent(lista cap,int info, lista *aux)
{ lista p;
if(cap==NULL)return NULL;
else { for(p=NULL,*aux=cap;(*aux)&&
((*aux)->inf-info);p=*aux,*aux=(*aux)->leg);
if((*aux)==NULL)return NULL;
return p;
}
}

int elimina_informatie(lista *cap,int info)


{ lista aux,p;
p=cautaprecedent(*cap,info,&aux);
if(aux==*cap)
{ *cap=(*cap)->leg;
free(aux);
return 1;
}
else
if(p)
{ p->leg=aux->leg;

115
Programarea calculatoarelor

free(aux);
return 1;
}
return 0;
}

Eliminarea nodului care succede primei componente al cărei câmp


de informaţie este cunoscut
Funcţia C elimina_dupa_informatie returnează valoarea 1 dacă eliminarea
este posibilă, altfel calculează valoarea 0. În situaţia în care informaţia infodat a
fost găsită în cîmpul corespunzător nodului nodul p (p nu este NULL) şi p are
succesor în listă, este realizată eliminarea nodului p->leg.
int elimină_dupa_informatie(lista cap,int *info, int
infodat)
{ lista aux,p;
p=cauta(cap,infodat);
if((p)&&(p->leg))
{ aux=p->leg;
p->leg=aux->leg;
*info=aux->inf;
free(aux);
return 1;
}
return 0;
}

7.3 Liste circulare


În anumite situaţii 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 structura de listă circulară.
Principalul avantaj al utilizării acestui tip de structură rezidă din posibilitatea
de accesare oricărui alt element al listei pornind din orice element. Dacă nodul
căutat este situat după nodul curent, este iniţiat un proces de căutare similar listelor
lineare. În caz contrar, nodul poate fi accesat prin parcurgerea listei de la primul
său element, care, în procesul de căutare, este atins după parcurgerea în întregime a
listei, începând de la nodul curent.
În continuare sunt prezentate funcţiile C pentru realizarea unor operaţii de
bază în lucrul cu liste circulare.

#include<stdio.h>
#include<conio.h>
#include<alloc.h>

typedef struct nod


{ int inf;
struct nod *leg;
} list, *lista;

116
Structuri dinamice de date. Liste

int inserare_la_inceput(lista *,int);


int stergere_la_inceput(lista *,int *);
int inserare_la_sfarsit(lista *,int);
int stergere_la_sfarsit(lista *,int *);
void parc(lista);
lista cauta(lista,int);

void main()
{
clrscr();
int n,info;
lista cap=NULL;
printf("Numarul de noduri:");
scanf("%i",&n);
printf("Introduceti informatiile\n");
for(int i=0;i<n;i++){
scanf("%i",&info);
if(inserare_la_inceput(&cap,info));
else
{printf("\n Spatiu insuficient \n");
return;
}
}
printf("\nLista rezultata\n");
parc(cap);
printf("\n\nLista dupa extragerea primului
element:\n");
if(stergere_la_inceput(&cap,&info)) parc(cap);
else printf("\nEroare: lista vida");
printf("\n\nInformatia nodului de introdus la
sfarsit:");
scanf("%i",&info);
if(inserare_la_sfarsit(&cap,info)){
printf("Lista rezultata\n");
parc(cap);
}
else
printf("\n Spatiu insuficient \n");
printf("\n\nLista dupa extragerea ultimului
element:");
if(stergere_la_sfarsit(&cap,&info)){
printf("\nInformatia extrasa %i\nLista
rezultata:",info);
parc(cap);
}
else printf("\nEroare:Lista vida");
getch();
}

void parc(lista cap)


{
lista p=cap;
if(cap){
printf("%i ",cap->inf);
for(p=p->leg;p-cap;p=p->leg)

117
Programarea calculatoarelor

printf("%i ",p->inf);
}
else printf("\nLista vida");
}

lista cauta(lista cap,int info)


{
if(cap==NULL)return NULL;
if(cap->inf==info) return cap;
for(lista p=cap->leg;p!=cap;p=p->leg)
if(p->inf==info) return p;
return NULL;}
int inserare_la_inceput(lista *cap,int info)
{
lista nou,ultim;
if(nou=(lista)malloc(sizeof(list))){
nou->inf=info;
nou->leg=*cap;
if(*cap){
for(ultim=*cap;ultim->leg!=(*cap);ultim=ultim->leg);
ultim->leg=nou;
}
else nou->leg=nou;
*cap=nou;
return 1;
}
return 0;
}
int stergere_la_inceput(lista *cap,int *info)
{
if(*cap){
lista aux=*cap;
*info=aux->inf;
for(lista ultim=*cap;
ultim->leg!=(*cap);ultim=ultim->leg);
if(ultim==(*cap)) *cap=NULL;
else{
*cap=(*cap)->leg;
ultim->leg=*cap;
}
free(aux);
return 1;
}
return 0;
}

int inserare_la_sfarsit(lista *cap,int info)


{
lista nou,ultim;
if(nou=(lista)malloc(sizeof(list))){
nou->leg=*cap;nou->inf=info;
if(*cap==NULL){
*cap=nou;
(*cap)->leg=*cap;
}

118
Structuri dinamice de date. Liste

else{
for(ultim=*cap;ultim->leg!=(*cap); ultim=ultim->leg);
ultim->leg=nou;
}
return 1;
}
return 0;
}

int stergere_la_sfarsit(lista *cap,int *info)


{
if (*cap){
if((*cap)->leg!=(*cap)){
for(lista pultim=*cap;
pultim->leg->leg!=(*cap);pultim=pultim->leg);
*info=pultim->leg->inf;
free(pultim->leg);
pultim->leg=(*cap);
}
else{
*info=(*cap)->inf;
free(*cap);
*cap=NULL;
}
return 1;
}
return 0;
}

7.4 Stive şi cozi


Aşa cum a rezultat din subcapitolele precedente, operaţiile de inserare şi
eliminare sunt permise la oricare dintre componentele unei liste. O serie de aplicaţii
pot fi modelate utilizând liste lineare în care introducerea şi respectiv eliminarea
informaţiilor este permisă numai la capete. În acest scop au fost introduse tipurile
de listă stivă şi coadă prin impunerea unui tip de organizare a aplicării operaţiilor
de inserare şi eliminare.

7.4.1 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.
Operaţiile de bază efectuate asupra unei stive pot fi realizate similar
cazului listelor dinamice lineare, ţinând cont că inserarea/extragerea unui element
sunt posibile numai în prima poziţie (vezi modulul de inserare la începutul unei
liste şi respectiv funcţia de extragere a primului nod dintr-o listă, prezentate
în §7.2).

119
Programarea calculatoarelor

7.4.2 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.
Implementarea unei liste de tip coadă poate fi efectuată atât printr-o
structură statică (masiv unidimensional), cât şi printr-o structură dinamică de tip
listă. Pentru optimizarea operaţiilor de inserare/extragere, în cazul implementării
cozilor prin structuri dinamice lineare, este necesară utilizarea a două informaţii:
adresa 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ă.
O variantă alternativă de implementare a unei liste de tip coadă dinamică
este obţinută prin considerarea unei liste circulare, cu memorarea adresei ultimului
element.
În continuare sunt prezentate operaţiile de inserare şi extragere a unei
informaţii dintr-o listă liniară de tip coadă.

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
typedef struct nod{
int inf;
struct nod *leg;
} list, *lista;

int inserare(lista *,lista *,int);


int extragere(lista *,lista *,int *);
void parc(lista);

void main()
{
clrscr();
int n,info;
lista cap=NULL,ultim=NULL;
printf("Numarul de noduri:");
scanf("%i",&n);
printf("Introduceti informatiile\n");
for(int i=0;i<n;i++){
scanf("%i",&info);
if(inserare(&cap,&ultim,info));
else
{printf("\n Spatiu insuficient \n");
return;
}
}

120
Structuri dinamice de date. Liste

printf("\nCoada rezultata\n");
parc(cap);
printf("\n\nCoada dupa o extragere:\n");
if(extragere(&cap,&ultim,&info)) parc(cap);
else printf("\nEroare: Coada vida");
getch();
}

void parc(lista cap)


{
if(cap){
printf("%i ",cap->inf);
parc(cap->leg);
}
}

int extragere(lista *cap,lista *ultim,int *info)


{
if(*cap){
lista aux=*cap;
*info=aux->inf;
if((*ultim)==(*cap)) *cap=*ultim=NULL;
else *cap=(*cap)->leg;
free(aux);
return 1;
}
return 0;
}

int inserare(lista *cap,lista *ultim,int info)


{
lista nou;
if(nou=(lista)malloc(sizeof(list))){
nou->inf=info;nou->leg=NULL;
if(*cap==NULL) *cap=*ultim=nou;
else{
(*ultim)->leg=nou;
(*ultim)=nou;
}
return 1;
}
return 0;}

121
8 Grafuri

Grafurile sunt structuri de date cu aplicaţii în multe domenii ale


informaticii, algoritmii pentru reprezentarea şi prelucrarea grafurilor fiind
consideraţi fundamentali în acest domeniu. În subcapitolul 8.1 sunt prezentate
principalele noţiuni ale domeniului, 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. Traversarea în adîncime a grafurilor determină obţinerea unei
clasificări a muchiilor, în funcţie de care pot fi derivate diferite proprietăţi ale
grafurilor. Verificarea conexităţii şi calculul drumurilor în grafuri sunt tratate în
subcapitolul 8.3. În finalul capitolului este studiată problema determinării
circuitelor şi ciclurilor în grafuri şi digrafuri.

8.1 Definiţii şi reprezentări ale grafurilor


Definiţia 8.1.1 Se numeşte graf sau graf neorientat 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.
Elementele mulţimii V se numesc vârfuri, iar obiectele mulţimii E se
numesc muchii. Dacă e ∈ E, e = (u,v) not.uv , vârfurile u şi v se numesc extremităţi
ale lui e, muchia e fiind determinată de vârfurile u şi v. Dacă e=uv ∈ E se spune că
vârfurile u, v sunt incidente cu muchia e.
Definiţia 8.1.2 Fie G=(V,E) graf. Vârfurile u, v sunt adiacente în G dacă
uv ∈ E.
Definiţia 8.1.3 Graful G=(V,E) este graf 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 8.1.4 Fie Gi =(V i,Ei), i=1,2 grafuri. G2 este un subgraf al grafului
G1 dacă V2 ⊆ V1 şi E 2 ⊆ E1 . G2 este un graf parţial al lui G1 dacă V2=V1 şi G2
este subgraf al lui G1.

122
Grafuri

Definiţia 8.1.5 Un digraf este o structură D=(V,E), unde V este o mulţime


nevidă de vârfuri, iar E este o mulţime posibil vidă de perechi ordonate cu
componente elemente distincte din V. Elementele mulţimii E sunt numite arce sau
muchii ordonate. Un graf direcţionat este o structură D=(V,E), unde V este o
mulţime nevidă de vârfuri, iar E este o mulţime posibil vidă de perechi ordonate cu
componente elemente din V, nu neapărat distincte. Evident, orice digraf este un
graf direcţionat.
Terminologia utilizată relativ la digrafuri este similară celei
corespunzătoare grafurilor. În continuare vom referi prin muchie şi elementele
mulţimii E ale unui graf direcţionat, în situaţia în care este tratat cazul unui graf
oarecare (neorientat sau direcţionat).
Definiţia 8.1.6 Se numeşte graf ponderat o structură (V,E,W), unde
G=(V,E) este graf şi W este o funcţie definită prin 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 8.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 .
Definiţia 8.1.8 Fie G=(V,E) un graf. Elementul v∈V se numeşte vârf izolat
dacă, pentru orice e ∈ E, u nu este incident cu e.

8.1.1 Moduri de reprezentare a grafurilor

Cea mai simplă reprezentare a unui graf este cea intuitivă, grafică; fiecare
vârf este figurat printr-un punct, respectiv 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 o determină

Exemple
8.1.1 Fie G=(V,E) graf, cu V={1,2,3,4,5,6}, E={(1,2),(1,3),(2,5),(3,5),(5,6)}.
O posibilă reprezentare grafică este,

2
4
6
5
3

123
Programarea calculatoarelor

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

4
2

3
5

8.1.3 Fie D=(V,E) graf direcţionat, V={1,2,3,4,5}, E={(1,2), (1,3), (1,5)


(2,5), (3,5), (4,4)}. Reprezentarea grafică este,
1

4
2

3
5

8.1.4 Fie G=(V,E,W) graf ponderat, V={1,2,3,4}, E={(1,2), (1,3), (1,4),


(2,3), (2,4)}, W((1,2))=5, W((1,3))=1, W((1,4))=7, W((2,3))=4, W((2,4))=2. O
posibilă reprezentare grafică este:
1

5 1
2 3
4
2
7

În scopul reprezentării grafurilor în memoria calculatorului sunt utilizate în


general următoarele structuri de date.

124
Grafuri

8.1.2 Reprezentarea matriceală

Grafurile, digrafurile şi grafurile direcţionate pot fi reprezentate prin


matricea de adiacenţă. Dacă G=(V,E ) este graf, digraf sau graf direcţionat
cu V = n , atunci matricea de adiacenţă A ∈ Mnxn({0,1}) are componentele,
⎧1, dacă (vi , v j ) ∈ E
aij = ⎨ ,
⎩0 , altfel
unde vi, vj reprezintă cel de-al i-lea, respectiv cel de-al j-lea nod din V. În
cazul unui graf neorientat, matricea de adiacenţă este simetrică.

Exemplu
8.1.5 Graful din exemplul 8.1.1, digraful din exemplul 8.1.2 şi graful
direcţionat din exemplul 8.1.3 sunt reprezentate prin matricele de adiacenţă,
⎛0 1 1 0 0 0⎞
⎜ ⎟ ⎛0 1 1 0 1⎞ ⎛0 1 1 0 1⎞
⎜1 0 0 0 1 0⎟ ⎜ ⎟ ⎜ ⎟
⎜0 0 0 0 1⎟ ⎜0 0 0 0 1⎟
⎜1 0 0 0 1 0⎟
A=⎜ ⎟ (8.1.1), A = ⎜ 0 0 0 0 1 ⎟ (8.1.2), A = ⎜ 0 0 0 0 1 ⎟ (8.1.3)
⎜0 0 0 0 0 0⎟ ⎜ ⎟ ⎜ ⎟
⎜ ⎟ ⎜1 0 0 0 0⎟ ⎜0 0 0 1 0⎟
⎜0 1 1 0 0 1⎟ ⎜ ⎟ ⎜ ⎟
⎝0 0 0 1 1⎠ ⎝0 0 0 0 0⎠
⎜0
⎝ 0 0 0 1 0 ⎟⎠

În cazul grafurilor ponderate, reprezentarea poate fi realizată prin matricea


ponderilor. Dacă G=(V,E,W) este graf ponderat, V = n , W ∈ Mnxn((0, ∞ )) are
componentele,
⎧W (( vi , v j )), dacă (vi , v j ) ∈ E
wi , 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
8.1.6 Presupunând că ponderile reprezintă costuri, matricea de reprezentare
⎛∞ 5 1 7⎞
⎜ ⎟
⎜5 ∞ 4 2⎟
a grafului din exemplul 8.1.4. este W = ⎜ .
1 4 ∞ ∞⎟
⎜ ⎟
⎜7 2 ∞ ∞ ⎟⎠

125
Programarea calculatoarelor

8.1.3 Reprezentarea tabelară

Reţinînd muchiile prin intermediul extremităţilor şi eventual valoarea


ponderii ei, se obţine reprezentarea tabelară, mai economică din punctul de vedere
al spaţiului de memorie necesar. Dacă graful conţine vârfuri izolate atunci este
necesară păstrarea acestora într-un vector suplimentar VS. Mulţimea muchiilor este
reţinută într-o matrice A cu E linii şi c coloane, unde c=2 dacă graful nu este
ponderat, altfel c=3. Î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
8.1.7 Graful din exemplul 8.1.1 poate fi reprezentat astfel, VS=(4),
⎛1 2⎞
⎜ ⎟
⎜1 3⎟
A = ⎜2 5⎟
⎜ ⎟
⎜3 5⎟
⎜5 6 ⎟⎠

⎛1 2⎞
⎜ ⎟
⎜1 3⎟
⎜1 5⎟
⎜ ⎟
8.1.8 Digraful din exemplul 8.1.2 este reprezentat prin A = ⎜ 2 5⎟ .
⎜ ⎟
⎜3 5⎟
⎜4 1⎟
⎜⎜ ⎟
⎝5 4 ⎟⎠

⎛1 2⎞
⎜ ⎟
⎜1 3⎟
⎜1 5⎟
8.1.9 Graful direcţionat din 8.1.3. este reprezentat prin A = ⎜ ⎟.
⎜2 5⎟
⎜ ⎟
⎜3 5⎟
⎜4 4 ⎟⎠

8.1.10 Graful ponderat din exemplul 8.1.4. nu are vârfuri izolate, deci este
⎛1 2 5⎞
⎜ ⎟
⎜1 3 1⎟
reprezentat prin intermediul matricei A = ⎜ 2 3 4⎟ .
⎜ ⎟
⎜1 4 7⎟
⎜ ⎟
⎝2 4 2⎠

126
Grafuri

8.1.4 Reprezentarea prin intermediul listelor

Această reprezentare permite utilizarea economică a spaţiului de memorare


şi, în anumite cazuri, implementări mai eficiente pentru anumite clase de algoritmi.
Vârfurile grafului sunt memorate într-o listă, fiecare nod al listei N conţinând o
referinţă spre lista vecinilor vârfului memorat ca informaţie în N.
Dacă graful nu este ponderat, el poate fi reprezentat prin structura 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ă referinţa spre începutul 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).

8.2 Modalităţi de parcurgere a grafurilor


Modalitatea de vizitare a tuturor vârfurilor grafului în care fiecare vârf al
grafului este vizitat o singură dată se numeşte parcurgere sau traversare. În acest
paragraf sunt prezentate metodele de parcurgere BF (în lăţime), DF (în adâncime)
şi metoda DF generalizată, notată DFG.
Primele două metode de parcurgere sunt aplicate grafurilor neorientate
respectiv grafurilor direcţionate şi presupun selectarea unui vârf iniţial v0 şi
identificarea acelor vârfuri ale grafului v cu proprietatea că există cel puţin un drum
de la vârful iniţial către v. Grafurile cu proprietatea că oricare două vârfuri sunt
conectate printr-un drum se numesc grafuri conexe şi sunt prezentate în § 8.3. Dacă
graful este conex, atunci prin aplicarea metodelor de parcurgere vor fi identificate
toate vârfurile grafului. Cele două modalităţi de parcurgere sunt prezentate în
continuare în cazul grafurilor neorientate, extinderea la digrafuri şi grafuri
direcţionate fiind imediată. Studiul proprietăţii metodei BF de a calcula distanţele
minim între orice vârf al grafului conectat de vârful iniţial şi vârful iniţial este
prezentat în cazul grafurilor oarecare.
Parcurgerea DFG presupune vizitarea tuturor vârfurilor unui graf sau graf
direcţionat prin aplicarea metodei DF tuturor vârfurilor care, după ultima traversare
DF, nu au fost încă vizitate.

127
Programarea calculatoarelor

8.2.1 Metoda de parcurgere BF (Breadth First)

Traversarea BF presupune parcurgerea î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
(teorema 8.2.1). Distanţa de la u la v, notată δ (u , v ) , este numărul de muchii ale
unui cel mai scurt u-v drum.
La momentul iniţial vârf curent este v0. Deoarece vârful curent la fiecare
moment trebuie să fie unul dintre vârfurile aflate la distanţă minimă de v0 se poate
proceda în modul următor: iniţial lui v0 i se asociază valoarea 0, d [v0 ] = 0 şi
fiecărui vârf v ≠ v0 i se asociază valoarea ∞ , d [v ] = ∞ . Dacă valoarea asociată
vârfului curent este m, atunci fiecăruia dintre vecinii acestuia de valoare ∞ li 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 vizitate, deci calculul se încheie.

Exemple
8.2.1 Fie graful,
1

2 3

6
4

7
5
şi v0=1.Valorile calculate prin aplicarea metodei prezentate sunt,

vârf 1 2 3 4 5 6 7
d
0 0 ∞ ∞ ∞ ∞ ∞ ∞
1 0 1 1 1 ∞ ∞ 1
2 0 1 1 1 2 2 1
0 1 1 1 2 2 1

128
Grafuri

8.2.2 Fie graful,


1 8

3
2

6
4 9 10

11
5 7

şi v0=1. Se observă că vârfurile 8, 9, 10 şi 11 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 11
d
0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞
1 0 1 1 ∞ 1 ∞ 1 ∞ ∞ ∞ ∞
2 0 1 1 2 1 2 1 ∞ ∞ ∞ ∞
0 1 1 2 1 2 1 ∞ ∞ ∞ ∞

Se observă că valorile lui d 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 d[v0] rezultată la terminarea
calculului este ∞ .
Fie G=(V,E) un graf, V = n . O alternativă de implementare a metodei BF
este construită 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);
• un vector c cu n componente, unde,
⎧1, dacă i a fost adăugat în coadă
ci = ⎨
⎩0, altfel
Componentele vectorului c sunt iniţializate cu valoarea 0.
Parcurgerea BF poate fi descrisă astfel,
• coada C este iniţializată cu vârful v0;
• cât timp C ≠ Ø, este extras şi vizitat un vârf i din coadă, apoi sunt
introduşi în coadă vecinii lui i care nu au fost deja introduşi (acele vârfuri k cu
proprietatea că c[k]=0 şi a[i][k]=1). Vârfurile i ce au fost introduse în coadă sunt
marcate prin c[i]=1.

129
Programarea calculatoarelor

Exemplu
8.2.3. Pentru graful din exemplul 8.2.1., aplicarea metodei de traversare BF
determină următoarea evoluţie,

c
t 1 2 3 4 5 6 7
t=1 1 0 0 0 0 0 0
t=2 1 1 1 1 0 0 1
t=3 1 1 1 1 1 0 1
t=4 1 1 1 1 1 1 1
t=5 1 1 1 1 1 1 1
t=6 1 1 1 1 1 1 1
t=7 1 1 1 1 1 1 1
t=8 1 1 1 1 1 1 1
C
t
t=1 1
t=2 2 3 4 7
t=3 3 4 7 5
t=4 4 7 5 6
t=5 7 5 6
t=6 5 6
t=7 6
t=8
Observaţie Deoarece graful din exemplul 8.2.1. este conex, traversarea BF
realizează vizitarea tuturor vârfurilor grafului. Aplicarea metodei BF grafului din
exemplul 8.2.2. nu determină vizitarea vârfurilor 8,9, 10 şi 11, deoarece acestea sunt
vârfuri neconectate cu vârful iniţial. Cu alte cuvinte, metoda BF aplicată unui graf
determină vizitarea tuturor vârfurilor care sunt conectate cu vârful iniţial selectat.

Sursa C pentru implementarea metodei BF este,


#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct nn
{ int inf;
struct nn *leg;
} nod,* pnod;
int insereaza_coada(pnod *head,pnod *tail,int info)
{
pnod nou;
if(nou=(pnod)malloc(sizeof(nod))){
nou->inf=info;
nou->leg=NULL;
if(*head==NULL) *head=nou;
else (*tail)->leg=nou;
*tail=nou;
return 1;
}
else return 0;
}

130
Grafuri

int extrage_coada(pnod *head,pnod *tail, int *info)


{
if(*head){
pnod aux=*head;
*info=(*head)->inf;
(*head)=(*head)->leg;
free(aux);
if(*head==NULL)*head=*tail=NULL;
return 1;
}
else return 0;}
void breadth_first(int v0,int a[10][10],int n)
{
pnod head=NULL;
pnod tail=NULL;
int c[10];
for(int i=0;i<n;c[i++]=0);
int r=insereaza_coada(&head,&tail,v0);
c[v0]=1;
while(head){
r=extrage_coada(&head,&tail,&i);
printf("\n%i",i+1);
for(int k=0;k<n;k++)
if((a[i][k]==1)&&(c[k]==0)){
r=insereaza_coada(&head,&tail,k);
c[k]=1;
}
}
}
void main()
{
int n,v0,a[10][10];
clrscr();
printf("Numarul de varfuri:");
scanf("%i",&n);
printf("\nMatricea de adiacenta\n");
for(int i=0;i<n;i++)
for(int j=0;j<i;j++){
scanf("%i",&v0);
a[j][i]=a[i][j]=v0;
}
for(i=0;i<n;i++)a[i][i]=0;
printf("\nVarful initial ");
scanf("%i",&v0);
printf("\nParcurgerea BF a grafului este");
breadth_first(v0,a,n);
}

În continuare sunt prezentate o serie de rezultate prin care este demonstrată


proprietatea parcurgerii BF de a calcula distanţa minimă de la orice vârf v conectat
de vârful iniţial v0 la v0.

Lema 8.2.1 Fie G=(V,E) un graf oarecare şi v0 ∈ V arbitrar. Atunci,


pentru orice muchie (u , v ) ∈ E , δ (v0 , v ) ≤ δ (v0 ,u ) + 1 .

131
Programarea calculatoarelor

Demonstraţie Dacă u este conectat de v0 în G, atunci, evident, şi v este


conectat de v0 în G. În acest caz, cel mai scurt drum de la v0 la v nu poate fi mai
lung decît cel mai scurt drum de la v0 la u prelungit cu muchia (u,v), deci afirmaţia
este demonstrată. În situaţia în care u nu este conectat de v0 în G, atunci, evident,
rezultă inegalitatea δ (v0 , v ) ≤ δ (v0 ,u ) + 1 .

Lema 8.2.2 Fie G=(V,E) un graf neorientat sau graf direcţionat şi v0 ∈ V


vârf iniţial al procedurii de traversare BF. Atunci orice v ∈ V vizitat, are loc
inegalitatea d [v ] ≥ δ (v0 , v ) .
Demonstraţie Afirmaţia este demonstrată prin inducţie după ordinea
vizitării BF a elementelor v ∈ V conectate cu v0 în G.
Dacă v = v0 , rezultă d [v ] = 0 şi, pentru orice
u ∈ V \ {v}, d [u ] = ∞ ≥ δ (u , v ) , deci afirmaţia este adevărată.
Fie v vârful vizitat ca rezultat al procesării vârfului u. Prin aplicarea
ipotezei inductive, d [u ] ≥ δ (v0 ,u ) , a rezultatului lemei 8.2.1 şi a procedurii de
parcurgere BF obţinem,
d [v ] = d [u ] + 1 ≥ δ (v0 ,u ) + 1 ≥ δ (v0 , v ) .
Deoarece vârful v nu a fost anterior găsit în lista vecinilor nici unui nod
studiat înaintea vârfului u, v este inserat în C.
Lema 8.2.3 Fie G=(V,E) un graf neorientat sau graf direcţionat şi
C = {v1 , v 2 ,...,v p } coada calculată la un moment al aplicării procedurii de
parcurgere BF. Atunci următoarele inegalităţile sunt verificate, [Cor,Lei şa]
[ ]
d v p ≤ d [v1 ] + 1
d [vi ] ≤ d [vi +1 ], ∀i = 1,..., p − 1 .

Teorema 8.2.1 Corectitudinea procedurii BF


Fie G=(V,E) graf neorientat sau graf direcţionat şi v0 ∈ V vârf iniţial al
procedurii de traversare BF. Atunci metoda BF calculează toate vârfurile v
conectate cu v0 în G şi, pentru orice v ≠ v0 , v ∈ V vizitat, cel mai scurt v0-v drum
este format dintr-un v0-u drum şi muchia (u,v), unde u este acel vârf prin procesarea
căruia este determinată vizitarea lui v.
Demonstraţie Fie Vk = {v ∈ V / δ (v0 , v ) = k } mulţimea vârfurilor situate
la distanţă k de v0. Rezultatul teoremei este demonstrat prin inducţie după k, cu
ipoteza inductivă,
Ik: ∀v ∈ Vk , există un singur moment al execuţiei procedurii BF în care
este determinată următoarea evoluţie,
d [v ] = k şi v ∈ C ;

132
Grafuri

dacă v ≠ v0 , vârful u care determină inserarea lui v în C este element al


mulţimii Vk −1 .
Pentru k = 0 , V0 = {v0 } . La momentul iniţial, C ← v0 şi d [v0 ] = 0 , deci
I este verificată.
Verificarea ipotezei Ik în condiţiile în care I0 ,…,Ik-1 sunt adevărate este
bazată pe următoarea observaţie. Pe tot parcursul execuţiei procedurii BF, C ≠ Ø
şi, dacă u ∈ C , atunci d [u ] şi vârful care a determinat procesarea lui u rămîn
constante.
{
Din lema 8.2.3 rezultă că, dacă C = v1 , v 2 ,..., v p , atunci }
d [vi ] ≤ d [vi +1 ], ∀i = 1,..., p − 1 .
Fie v ∈ Vk , k ≥ 1 . Din proprietatea de
monotonie şi ipoteza Ik-1, rezultă că v a fost inserat în C după ce toate vârfurile
u ∈ Vk −1 au fost deja inserate în coadă. Deoarece δ (v0 , v ) = k , obţinem că există
un v0-v drum de lungime k şi u ∈ Vk −1 astfel încât (u , v ) ∈ E . Fără a pierde din
generalitate, vom presupune că u este primul vârf din Vk −1 inserat în C.
La momentul în care vârful u devine prim element al cozii C, toate
vârfurile vecine cu u în G sunt inserate în C, deci şi vârful v. Rezultă că
d [v ] = d [u ] + 1 = k , unde u este acel vârf care precede v pe un cel mai scurt v0-v
drum.
Observaţii
1. Demonstrarea teoremei de corectitudine a parcurgerii BF stabileşte şi o
modalitate de calcul al unui cel mai scurt v0-v drum astfel. Pentru orice v ∈ V
conectat cu v0 în G, fie p[v ] ∈ V vârful a cărui procesare a determinat inserarea lui
v în C. Un v0-v drum de lungime minimă este v0- p[v ] drumul cel mai scurt
“prelungit” cu muchia ( p[v ], v ) .
2. Aplicarea metodei BF unui graf oarecare G determină obţinerea unui
arbore (vezi capitolul 9) Gp, numit subgraful predecesorilor definit de BF pe G,
(
unde G p = V p , E p şi )
V p = {v ∈ V / p[v ] ∈ V } Υ {v0 } , E p = {( p[v ], v ) ∈ E / v ∈ V \ {v0 }} .

Exemplu
8.2.4 Prin aplicarea procedurii BF grafului din 8.2.1, obţinem,
1

2
3 4 7

5 6

133
Programarea calculatoarelor

8.2.2 Metoda de parcurgere DF (Depth First)


Ideea metodei DF revine la parcurgerea în adâncime a grafurilor.
Considerând v0 vârf iniţial şi M mulţimea vârfurilor vizitate de procedură, pentru
vizitarea vecinilor este considerat unul din vârfurile din M cu proprietatea că
lungimea drumului calculat de metodă până la vârful iniţial v0 este maximă.
Implementarea acestei metode 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 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:
⎧1, dacă i a fost vizitat
ci = ⎨
⎩0, altfel
Componentele vectorului c vor fi iniţializate cu valoarea 0.
Exemple
8.2.5 Pentru graful,
1

2 3

6
4

7
5
şi v0=1, prin aplicarea metodei descrise, rezultă următoarea evoluţie.
c
1 2 3 4 5 6 7
t
t=1 1 0 0 0 0 0 0
t=2 1 1 1 1 0 0 1
t=3 1 1 1 1 0 1 1
t=4 1 1 1 1 0 1 1
t=5 1 1 1 1 1 1 1
t=6 1 1 1 1 1 1 1
t=7 1 1 1 1 1 1 1
t=8 1 1 1 1 1 1 1
S
t
t=1 1
t=2 7 4 3 2
t=3 6 4 3 2
t=4 4 3 2
t=5 5 3 2
t=6 3 2
t=7 2
t=8

134
Grafuri

Ordinea în care sunt vizitate vârfurilor corespunzător acestei variante de


parcurgere DF este: 1, 2, 3, 4, 7, 6, 5.
8.2.6 Pentru graful din exemplul 8.2.2 vârfurile 8,9,10 care nu sunt
conectate cu vârful iniţial nu vor fi vizitate nici prin aplicarea metodei DF. Ordinea
în care sunt vizitate vârfurile corespunzătore acestei variante este: 1, 2, 3, 4, 6, 7, 5.
O variantă de implementare a metodei DF rezultă prin gestionarea stivei S
în modul următor. 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 el este eliminat din
stivă şi 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 iniţial
ales.

Exemplu
8.2.7 Pentru graful,
1

2 3

6
4

7
5
şi v0=1, prin aplicarea metodei descrise, rezultă următoarea evoluţie.
c
1 2 3 4 5 6 7
t
t=1 1 0 0 0 0 0 0
t=2 1 1 0 0 0 0 0
t=3 1 1 0 1 0 0 0
t=4 1 1 1 1 0 0 0
t=5 1 1 1 1 0 1 0
t=6 1 1 1 1 0 1 1
t=7 1 1 1 1 0 1 1
t=8 1 1 1 1 0 1 1
t=9 1 1 1 1 0 1 1
t=10 1 1 1 1 1 1 1
t=11 1 1 1 1 1 1 1
t=12 1 1 1 1 1 1 1
t=13 1 1 1 1 1 1 1
t=14 1 1 1 1 1 1 1

135
Programarea calculatoarelor

S
t
t=1 1
t=2 2 1
t=3 4 2 1
t=4 3 4 2 1
t=5 6 3 4 2 1
t=6 7 6 3 4 2 1
t=7 6 3 4 2 1
t=8 3 4 2 1
t=9 4 2 1
t=10 5 4 2 1
t=11 4 2 1
t=12 2 1
t=13 1
t=14

Ordinea în care sunt vizitate vârfurile corespunzător acestei variante este:


1, 2, 4, 3, 6, 7, 5.
Următoarea sursă C implementează varianta precedentă de parcurgere DF.

#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct nn{
int inf;
struct nn *leg;
}nod,* pnod;

int insereaza_stiva(pnod *head,int info)


{
pnod nou;
if(nou=(pnod)malloc(sizeof(nod))){
nou->inf=info;
nou->leg=*head;
*head=nou;
return 1;
}
else return 0;
}

int extrage_stiva(pnod *head,int *info)


{
if(head){
pnod aux=*head;
*info=(*head)->inf;
(*head)=(*head)->leg;
free(aux);
return 1;
}
else return 0;}
void depth_first(int v0,int a[10][10],int n)
{

136
Grafuri

pnod head=NULL;
int c[10];
for(int i=0;i<n;c[i++]=0);
int r=insereaza_stiva(&head,v0);
c[v0]=1;
printf("\n%i",v0+1);
while(head){
r=extrage_stiva(&head,&i);
for(int k=0;k<n;k++)
if((a[i][k]==1)&&(c[k]==0)){
r=insereaza_stiva(&head,k);
c[k]=1;printf("\n%i",k+1);
}
}
}

void main()
{
int n,v0,a[10][10];
clrscr();
printf("Numarul de varfuri:");scanf("%i",&n);
printf("\nMatricea de adiacenta\n");
for(int i=0;i<n;i++)
for(int j=0;j<i;j++){
scanf("%i",&v0); a[j][i]=a[i][j]=v0;
}
for(i=0;i<n;i++)a[i][i]=0;
printf("\nVarful initial ");scanf("%i",&v0);
printf("\nParcurgerea DF a grafului este");
depth_first(v0,a,n);
}

8.2.3 Parcurgerea în adâncime în varianta generalizată – DFG

Următoarea variantă de implementare a parcurgerii în adâncime, DFG,


determină vizitarea tuturor vârfurilor grafului analizat (considerat neorientat sau
direcţionat), indiferent dacă acesta este conex sau neconex.
Fie G=(V,E) un graf oarecare. Vom presupune în continuare că niciun vârf
din V nu este etichetat cu informaţia 0. Implementare traversării DFG utilizează
următoarele structuri,
• A, matricea de adiacenţă a grafului;
• p, vectorul predecesorilor (vezi § 8.2.1);
• f, vectorul care marchează încheierea analizării listei vârfurilor
vecinilor nodului curent;
• mark, definit pentru orice v ∈ V prin,
0, dacă v nu a fost încă analizat
mark[v]= 1, dacă v este procesat la momentul curent
2, dacă consultarea lui v este încheiată
• d, definit pentru orice v ∈ V prin d[v]=t, unde t este momentul de timp
la care este iniţiată analiza vârfului v.

137
Programarea calculatoarelor

Considerând t variabilă publică desemnând momentul prelucrării,


procedura DFG poate fi descrisă prin intermediul următoarelor funcţii.

void DFG(graf G)
{
for( ∀u ∈ V ){
mark[u]=0;
p[u]=0;
}
t=0;
for( ∀u ∈ V )
if(!mark[u])DF_Visit(u);
}

void DF_Visit(varf u)
{
mark[u]=1;
d[u]=t++;
for( v ∈ V :A[u][v]==1)
if(!mark[v]){
p[v]=u;
DF_Visit(v);
}
mark[u]=2;
f[u]=t++;
}

Prin aplicarea parcurgerii în adâncime în varianta generalizată sunt


obţinute informaţii suplimentare ale grafului de intrare. Metoda DFG determină
obţinerea subgraful predecesorilor, G p , de tip graf pădure (componentele conexe –
vezi § 8.3- sunt arbori - vezi capitolul 9). Fiecare componentă conexă a lui G p este
construită prin executarea modulului DF_Visit. De asemenea, vârfurile u , v ∈ V au
proprietatea că u = p[v ] dacă şi numai dacă funcţia DF_Visit(v) a fost apelată la
momentul căutării în lista vecinilor vârfului u.
O altă proprietate importantă a metodei de parcurgere DFG este aceea că,
după încheierea calculului, este determinată o structură de tip paranteză astfel.
Dacă momentul selectării vârfului u pentru procesare este marcat prin “(u” şi
momentul încheierii prelucrării lui u este notat “u)”, atunci istoricul traversării
DFG pentru calculul fiecărui arbore din G p poate fi reprezentat prin intermediul
unei expresii corecte din punct de vedere al parantezării.

Teorema 8.2.2 Corectitudinea parantezării determinată de aplicarea


metodei DFG
Fie G=(V,E) un graf sau un graf direcţionat. Prin aplicarea traversării DFG
este obţinut următorul rezultat. Pentru orice u , v ∈ V , una şi numai una din
următoarele afirmaţii este adevărată, [Cor,Lei şa]

138
Grafuri

1) [d [u ], f [u ]] şi [d [v], f [v]] sunt disjuncte;


2) [d [u ], f [u ]] ⊂ [d [v ], f [v]] şi u este un descendent al lui v în arborele
corespunzător din G p ;
3) [d [u ], f [u ]] ⊃ [d [v ], f [v]] şi u este un ancestor al lui v în arborele
corespunzător din G p .

Observaţie
Fie G=(V,E) un graf sau un graf direcţionat. Pe baza procedurii DFG poate
fi realizată următoarea clasificare a elementelor e = (u , v ) ∈ E ,
1) muchii de tip arbore în DF- graful pădure G p , etichetate cu T: (u , v )
are eticheta T dacă procesarea vârfului v a fost decisă ca rezultat al
testării existenţei muchiei e;
2) muchii de tip înapoi, cu etichetă B: (u , v ) este muchie B dacă v este
ancestorul lui u într-o componentă conexă a DF- grafului pădure G p ;
3) muchii de tip înainte, notate cu F: acele muchii (u , v ) , neetichetate cu
T şi în care v este descendent al lui u într-o componentă conexă a DF-
grafului pădure G p ;
4) muchii de tip trecere, etichetate cu C: toate muchiile (u , v ) rămase
neetichetate după încheierea etichetării cu T, B şi F.
Teorema 8.2.3
Fie G=(V,E) un graf neorientat. Orice element e ∈ E este fie de tip T, fie
de tip B. [Cor,Lei şa]
Teorema 8.2.4 Criteriu de aciclicitate pentru grafuri direcţionate
Un graf direcţionat este aciclic (vezi §8.4) dacă şi numai dacă niciuna
dintre muchii nu este de tip B. [Cor,Lei şa]

Exemple
8.2.8. Pentru graful
1 8

3
2

6
4 9 10

5 7
obţinem,
1) ordinea de parcurgere DFG a vârfurilor: 1,2,3,4,6,7,5,8,9,10

139
Programarea calculatoarelor

2) graful pădure G p ,
8
1
2 9
3
10
4

6 5

3) structurile paranteză: (1 (2 (3 3) (4 (6 (7 7) 6) (5 5) 4) 2) 1) şi
(8 (9 (10 10) 9) 8)
4) clasificarea muchiilor,
8
T
T 1 T
2 9
3
T
10 T
4 B
B T
T
6 5 B
T
7

8.2.9 Fie graful direcţionat,

6 7 1 2

5 8 4 3

Prin parcurgerea DFG obţinem următoarea ordonare: 1,7,6,8,5,2,4,3.


Subgraful predecesorilor este format din următoarele componente,

140
Grafuri

1 2

4 3
7

6 8

Structurile paranteză: (1 (7 (6 (5 5) 6) (8 8) 7) 1) şi (2 (4 4) (3 3) 2).


Clasificarea muchiilor grafului direcţionat este,

1 2
B
T T T
7 F 4 3
C
T C
T
B 6
8
T
C

8.3 Drumuri în grafuri. Conexitate


8.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 §8.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 8.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 drum
trivial, un drum Γ cu l(Γ)=0.
Definiţia 8.3.2. Fie Γ: u0, u1,..,un un drum în graful G=(V,E). Γ este un
drum închis dacă u0=un; în caz contrar, Γ se numeşte drum deschis. Drumul Γ este

141
Programarea calculatoarelor

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.

Exemplu
8.3.1 Pentru graful,
v2

v4
v5

v1 v3
Γ1: v1, v2, v3, v2, v5, v3, v4 este un v1- v4 drum care nu este proces;
Γ2: v1, v2, v5, v1, v3, v4 este un v1- v4 proces care nu este drum elementar;
Γ3: v1, v3, v4 este un v1- v4 drum elementar.

Definiţia 8.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.

Observaţie
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
⎧u j u j +1 ...u n , dacă i = 0

Γ ' : ⎨u0 u1 ...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-um drum elementar.

142
Grafuri

Exemplu
8.3.2 În graful,

v2 v6 v7

v1 v4 v5 v8 v10

v3 v9

dacă Γ: v1, v2, v4, v5, v3, v1, v2, v5, v6, v7, v8, v9, v5, v9, v8, v10, atunci Γ1: v1, v2,
v5, v9, v8, v10, Γ2: v1, v2, v4, v5, v9, v8, v10 sunt v1-v10 subdrumuri elementare.

8.3.2 Matricea existenţei drumurilor; algoritmul Roy-Warshall

Lema 2.3.1 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 ) . ( )
Demonstraţie
Demonstrarea acestei afirmaţii este realizată prin inducţie după p
Pentru p=1, deoarece pentru orice 1 ≤ i , j ≤ n există cel mult un vi-vj drum
de lungime 1 şi dacă există, fie acesta Г: vi, vj. Rezultă că numărul vi-vj drumurilor
de lungime 1 este egal cu aij(1) .
( )
Presupunem că Ap-1 = a ij( p −1) are proprietatea că pentru toţi 1 ≤ i , j ≤ n ,
(a ( p −1 )
ij ) este egal cu numărul v -v drumurilor de lungime p-1 în G.
i j

Cum A =A A = (a ( ) ) , rezultă că, 1 ≤ i , j ≤ n , a


p
p p-1
ij
p
= ∑a ( p)
ij
( p −1 )
ik a kj .
k =1
Orice vi-vj drum de lungime p în G conţine un vi-vk drum de lungime p-1 pentru un
anume vk adiacent cu vj şi reciproc, pentru orice vk adiacent cu vj oricărui vi-vk drum
de lungime p-1 îi corespunde un vi-vj drum de lungime p.
Din relaţia care caracterizează elementele aij( p ) , utilizînd ipoteza ( )
inductivă, rezultă afirmaţia enunţată mai sus.
Definiţia 8.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 }.

143
Programarea calculatoarelor

Dacă A=(aij) ∈ Mn({0,1)}, se notează A = a ij { ( ); k ≥ 1} secvenţa de


k (k)

matrice definită prin:


(1 ) k ( k −1 )
A = A, A = A ⊗ A
, ∀k ≥ 2 .
Dacă A este matricea de adiacenţă a unui graf G=(V,E), atunci pentru
fiecare k, 1 ≤ k ≤ n − 1,
(k ) ⎧1, dacă există drum de la i la j de lungime k
a ij =⎨
⎩0 , altfel
(1) (2) ( n −1 )
Matricea M = A ⊕ A ⊕ Κ ⊕ A se numeşte matricea existenţei
drumurilor în graful G. Semnificaţia componentelor matricei M este:
⎧0 , dacă nu există vi − v j drum în G
∀1 ≤ i , j ≤ n , mij = ⎨
⎩1, altfel
Exemplu
8.3.3 Pentru graful,
2

4
⎛0 1 1 1⎞ ⎛1 0 1 1⎞ ⎛1 1 1 1⎞ ⎛1 1 1 1⎞
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1 0 0 0⎟ 2 ⎜0 1 1 1⎟ 3 ⎜1 0 1 1⎟ ⎜1 1 1 1⎟
A=⎜ , A =⎜ , A =⎜ , M =⎜
1 0 0 1⎟ 1 1 1 1⎟ 1 1 1 1⎟ 1 1 1 1⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1 0 1 0 ⎟⎠ ⎜1 1 1 1⎟⎠ ⎜1 1 1 1⎟⎠ ⎜1 1 1 1⎟⎠
⎝ ⎝ ⎝ ⎝

Observaţie
Calculul matricei existenţei drumurilor permite verificarea dacă 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.
void Roy_Warshall (unsigned char a[10][10],unsigned n,unsigned char
m[10][10])
{int i,j,k;
for (i=0;i<n;i++)
for (j=0;j<n;j++)
m[i][j]=a[i][j];
for (j=0;j<n;j++)
for (i=0;i<n;i++)
if(m[i][j])
for (k=0;k<n;k++)
if (m[i][k]<m[k][j]) m[i][k]=m[k][j];}

144
Grafuri

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.

8.3.3 Componente conexe ale unui graf

Definiţia 8.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 8.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.

Exemplu
8.3.4 Componentele conexe ale grafului
1
5

2
6

4
3

sunt: C1={1,2,3}, C2={4,5}, C3={6}.

Observaţii
1) Un graf este conex dacă şi numai dacă numărul componentelor sale
conexe este 1.
2) Mulţimile de vârfuri corespunzătoare oricăror două componente
conexe distincte sunt disjuncte. Rezultă că mulţimile de vârfuri
corespunzătoare componentelor conexe ale unui graf formează o
partiţie a mulţimii vârfurilor grafului.

Problema determinării componentelor conexe corespunzătoare unui graf


poate fi rezolvată în modul următor. Iniţial, este selectat drept vârf curent un vârf al
grafului pentru care este calculată componenta conexă care îl conţine. Dacă există
vârfuri care nu aparţin componentei conexe determinate, este ales drept vârf curent
unul dintre aceste vârfuri. În continuare este aplicată aceeaşi metodă, până când au
fost găsite toate componentele conexe ale grafului.
Determinarea componentei conexe care conţine un vârf v0 dat poate fi
realizată pe baza următorului algoritm.
Pentru G=(V,E), V = n , n ≥ 1 şi v0 ∈ V, paşii algoritmului sunt:
Pas1: V0={v0}; E0= Φ ; i=0;

145
Programarea calculatoarelor

Pas 2: repetă Pas 3 până cînd Vi=Vi-1 şi Ei=Ei-1


Pas 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 Gi=(Vi,Ei), componenta conexă din care face parte v0.

Exemplu
8.3.5 Pentru graful,

1 7 3
2

4 5 8 9 6

Aplicarea algoritmului descris pentru v0=1, determină următoarea evoluţie:

I Vi Ei
i=0 {1} Ø
i=1 {1,2,4} {(1,2),(1,4)}
i=2 {1,2,4,7,8,5} {(1,2),(1,4),(2,7),(2,8),(7,8),(4,5),(4,7),(5,8)}

8.3.4 Drumuri de cost minim

Definiţia 8.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(Γ ), Γ ∈ Duv } , unde Duv desemnează mulţimea tuturor
u-v drumurilor elementare din G. Dacă Γ ∈ Duv este astfel încât D(u,v)=L(Γ),
drumul Γ se numeşte drum de cost minim.

Observaţie
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 unui cel mai scurt drum
între cele două vârfuri. În acest caz D este o distanţă pe mulţimea vârfurilor.

146
Grafuri

Algoritmul Dijkstra
Următorul algoritm a fost propus de către 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 G=(V,E,w) un 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 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 u0 , S = min D(u0 ,u ) + w( uv ); u ∈ S , v ∈ S ,uv ∈ E . }
( )
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 a câte unui cel mai ieftin u0-v drum, algoritmul
consideră o etichetare dinamică a vârfurilor grafului.Eticheta vârfului v este
(L(v),u), unde L(v) este lungimea unui cel mai ieftin u0-v drum 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:
Pas 1: i=0; S0={u0}; L(u0)=0, L(v)= ∞ pentru toţi v ∈ V, v≠u0. Dacă n=1
atunci stop
Pas 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).
Pas 3: Se determină d=min{L(v), v∈ Si } şi se alege ui+1∈ Si astfel încât
L(ui+1)=d.
Pas 4: Si+1=Si ∪ {ui+1}
Pas 5: i=i+1. Dacă i=n-1, atunci stop. Altfel, reia Pas 2.
Observaţie
Dacă (V,E,w) 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
8.3.6 Fie graful ponderat,
1

5 1

2 9
3
16
2
5 5
4

147
Programarea calculatoarelor

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ă nicio etichetă şi nicio 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 este 1 2 3 4 5
calculată w-distanţa
D(1,v), eticheta lui v 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 şamd, până se ajunge la eticheta 1. 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.

148
Grafuri

Următoarea sursă C implementează algoritmul Dijkstra.


#include<stdio.h>
#include<conio.h>
#include<alloc.h>

typedef struct{
int predv;
float L;
} eticheta;
void creaza(int *s,int *sb,int nv,int u0)
{
s[0]=u0;
for(int j=0,i=0;i<nv;i++)
if(i-u0)sb[j++]=i;
}
void modifica(int *s,int *sb,int ui, int *ns, int *nb)
{
s[*ns]=ui;
(*ns)++;
for(int i=0;i<*nb;i++)
if(sb[i]==ui){
for(int j=i+1;j<*nb;j++)
sb[j-1]=sb[j];
(*nb)--;
return;
}
}
eticheta *Dijkstra(float w[][50],int nv,int u0)
{
eticheta *r=(eticheta *)malloc(nv*sizeof(eticheta));
for(int i=0;i<nv;i++)r[i].L=1000;
r[u0].L=0;
r[u0].predv=u0;
int s[50],sb[50],ns=1,nb=nv-1;
creaza(s,sb,nv,u0);
for(i=0;i<nv-1;i++){
float dmin=1000;
for(int j=0;j<nb;j++)
for(int k=0;k<ns;k++)
if(r[sb[j]].L>r[s[k]].L+w[sb[j]][s[k]]){
r[sb[j]].L=r[s[k]].L+w[sb[j]][s[k]];
r[sb[j]].predv=s[k];
}
int ui;
for(j=0;j<nb;j++)
if(r[sb[j]].L<dmin){
dmin=r[sb[j]].L;
ui=sb[j];
}
modifica(s,sb,ui,&ns,&nb);
}

149
Programarea calculatoarelor

return r;
}
void main()
{
int n,i,j;
clrscr();
printf("Numarul de varfuri");
scanf("%i",&n);
printf("Matricea ponderilor:\n");
float w[50][50];
for(i=0;i<n;i++)
for(j=0;j<n;j++)
scanf("%f",&w[i][j]);
int u0;
printf("\nVarful initial:");
scanf("%i",&u0);
u0--;
eticheta *rez=Dijkstra(w,n,u0);
for(i=0;i<n;i++){
printf("Distanta de la vf. %i la vf. %i este
%7.2f\n",u0+1,i+1,rez[i].L);
printf("Un drum de cost minim este:");
printf("%i, ",i+1);
j=rez[i].predv;
while(j-u0){
printf("%i, ", j+1);
j=rez[j].predv;
}
printf("%i\n\n",u0+1);
}
free(rez);
getch();
}

În anumite aplicaţii este necesară exclusiv determinarea 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 funcţii (similară
algoritmului Roy-Warshall),

void Roy_Floyd (float w[10][10],unsigned n,float


d[10][10],float MAX)
{int i,j,k;
for (i=0;i<n;i++)
for (j=0;j<n;j++)
d[i][j]=w[i][j];
for (j=0;j<n;j++)
for (i=0;i<n;i++)
if(d[i][j]<MAX)

150
Grafuri

for (k=0;k<n;k++)
if (d[i][k]>d[i][j]+d[j][k])
d[i][k]=d[i][j]+d[j][k];
}

Matricea D calculată de algoritm este matricea w-distanţelor D(u,v) în


graful ponderat conex (V,E,w); pentru orice 1 ≤ i , j ≤ n
⎧ D( vi , v j ), vi , v j sunt conectate
d ij = ⎨
⎩∞ , altfel
Într-adevăr, procedura realizează calculul dinamic al w-distanţei între
oricare două vârfuri i şi k, astfel: dacă există un drum 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 costul drumului vechi, deci
w-distanţa între i şi k trebuie reactualizată la dij+djk.

Algoritmul Yen
Algoritmul propus de Yen pentru calculul tuturor w-distanţelor într-un graf
ponderat este mai eficient din punctul de vedere al volumului de operaţii decît
algoritmul Roy-Floyd. Fie (V,E,w) un graf ponderat şi 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,

Pas 1: D=W
Pas 2: i=1; λ(k)=0, b(k)=0; λ(j)=0, pentru toţi 1 ≤ j ≤ n , j ≠ k
Pas 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
Pas 4: Dacă i<n, reia Pas 3, altfel stop.

La terminarea algoritmului componentele vectorului B sunt respectiv egale


cu w-distanţa de la vârful vk la orice alt vârf al grafului. Într-adevăr, componentele
egale cu 1 ale vectorului λ indică, la fiecare reluare a pasului 3, vârfurile grafului
pentru care nu s-a calculat încă w-distanţa la vârful vk. După fiecare efectuare a
etapei 3, dacă j0 a fost selectat, atunci B(j0)=D(vk, v j0 ).

151
Programarea calculatoarelor

Exemplu
8.3.7 Fie graful
1

4
3
5 7
2
2
1
5
4 3
4
Se consideră vk=1.

⎛∞ 3 2 7 4 ⎞
⎜ ⎟
⎜3 ∞ 5 ∞ ∞⎟
Pas 1: D = ⎜ 2 5 ∞ 4 1⎟
⎜ ⎟
⎜7 ∞ 4 ∞ ∞⎟
⎜4 ∞ 1 ∞ ∞ ⎟⎠

Pas 2: i=1, λ=(0,1,1,1,1); B(1)=0


⎛∞ 3 2 6 3 ⎞
⎜ ⎟
⎜3 ∞ 5 ∞ ∞⎟
Pas 3: j0=3, B(3)=2, λ=(0,1,0,1,1); D = ⎜ 2 5 ∞ 4 1 ⎟ ; i=2
⎜ ⎟
⎜7 ∞ 4 ∞ ∞⎟
⎜4 ∞ 1 ∞ ∞ ⎟⎠

Pas 4: i<5, reia Pas 3

Pas 3: j0=2, B(2)=3, λ=(0,0,0,1,1); nicio modificare în matricea D; i=3


Pas 4: i<5, reia Pas 3

Pas 3: j0=5, B(5)=3, λ=(0,0,0,1,0); nicio modificare în matricea D; i=4


Pas 4: i<5, reia Pas 3

Pas 3: j0=4, B(4)=6, λ=(0,0,0,0,0); nicio modificare în matricea D; i=5


Pas 4: i=5, stop.

152
Grafuri

8.4 Circuite şi cicluri în grafuri şi în digrafuri


Definiţia 8.4.1 Fie G=(V,E) un graf netrivial, u, v∈V şi Γ un u-v drum în
G. Γ se numeşte proces dacă toate muchiile drumului Γ sunt distincte. Drumul
Γ este trivial dacă Γ : u,u.
Definiţia 8.4.2 Drumul Γ este un circuit dacă Γ este un proces netrivial
închis.
Definiţia 8.4.3 Circuitul Γ : v1, v2,…., vn, v1 cu n≥3 este un ciclu al
grafului, dacă, pentru orice i, j, cu 1 ≤ i , j ≤ n , i ≠ j , rezultă vi≠vj.
Observaţie
Orice ciclu este un drum elementar închis.
Definiţia 8.4.4 Graful G este aciclic dacă nu există cicluri în G.
Observaţie
Într-un digraf D noţiunile de proces, circuit, ciclu sunt definite ca şi în
cazul grafurilor.

Exemple
8.4.1 În graful,

v1
v4
v2

v3
v5 v6

Γ 1: v1, v2, v3, v6, v5 este un proces;


Γ 2: v1, v2, v3, v6, v5, v3, v4, v1 este un circuit şi nu este ciclu;
Γ 3: v1, v3, v5, v4, v1 este un ciclu.

8.4.2 Drumul Γ : v1,v2,v4,v3,v1 este un ciclu, deci graful conţine cicluri.

v1 v2

v3 v4

153
Programarea calculatoarelor

8.4.3 Digraful,

V1 V2

V3 V4

nu conţine cicluri.
Definiţia 8.4.5 Fie D=(V,E) un digraf. Funcţiile grad exterior, odD,
respectiv grad interior, idD, sunt definite prin, od D : V → N ; id D : V → N ,
∀u ∈ V , od D (u ) = {v / v ∈ V , uv ∈ E} ,
∀u ∈ V , id D (u ) = {v / v ∈ V , vu ∈ E}
Funcţia grad, notată degD, este definită astfel,
deg D : V → N, ∀u ∈ V, deg D (u ) = id D (u ) + od D (u ) .

Algoritmul Marimont
Procedura Marimont verifică dacă un digraf D=(V,E), V = n , este sau nu
aciclic. La terminarea calculului este afişat mesajul “DA”, dacă digraful D este
aciclic, respectiv “NU”, în caz contrar. Descrierea pe paşi a algoritmului Marimont
este,
Pas 1: V0=V, E0=E, D0=(V0,E0)
Pas 2: Dacă od D 0 (v ) ≥ 1 pentru toţi v∈V0, scrie “NU”, stop (dacă toate
vârfurile sunt extremităţi iniţiale ale măcar unui arc, atunci există cicluri în D0);
altfel, continuă.
Pas 3: Selectează v∈V0 cu od D 0 (v ) = 0 ;V0=V0\{v}; E0=E0-{e/ e∈E0, e
incidentă cu v în D0}; D0=(V0,E0)
Pas 4: Dacă V0≠Ø, atunci reia pasul 2; altfel scrie “DA”, stop.

Exemple
8.4.4 Pentru digraful,

1
e1 e2
2
4 e3
e4
3 e5
5
evoluţia algoritmului Marimont este,

154
Grafuri

Pas 1: V0={1,2,3,4,5}, E0={e1,e2,e3,e4,e5}


Pas 2: od D 0 (5) = 0 , continuă

1
e1 e2
2
4 e3
e4
3

Pas 3: Selectează vârful 5, elimină 5 din V0, elimină arcul e5 E0


Pas 4: reia de la pasul 2
Pas 2: od D 0 (i ) = 1 pentru toţi i din V0 ={1,2,3,4}, scrie “NU”, stop.

8.4.5 Pentru digraful D:

1
6
e7 e4 e1
5 4 e5
e9 e6 2
e8 e3 e2
7 3

algoritmul Marimont determină următoarea secvenţă de operaţii:


Pas 1: V0={1,2,3,4,5,6,7}, E0={e1,e2,e3,e4,e5,e6,e7,e8,e9}
Pas 2: od D 0 (7 ) = 0 , continuă
Pas 3: Selectează vârful 7, elimină 7 din V0, elimină arcele e8 şi e9 din E0
D0:

1
6
e7 e4 e1
5 4 e5
e6 2
e3 e2
3

Pas 4: reia de la pasul 2

Pas 2: od D 0 (6 ) = 0 , continuă

155
Programarea calculatoarelor

Pas 3: Selectează vârful 6, elimină 6 din V0, elimină arcul e7 din E0


D0:
1
e4 e1
5 4 e5
e6 2
e3 e2
3

Pas 4: reia de la pasul 2


Pas 2: od D 0 (5) = 0 , continuă
Pas 3: Selectează vârful 5, elimină 5 din V0, elimină arcul e6 din E0
D0:

1
e4 e1
4 e5
2
e3 e2
3

Pas 4: reia de la pasul 2


Pas 2: od D 0 (4 ) = 0 , continuă
Pas 3: Selectează vârful 4, elimină 4 din V0, elimină arcele e4, e5 şi e3 din
E0
D0:
1
e1
2
e2
3

Pas 4: reia de la pasul 2


Pas 2: od D 0 (3) = 0 , continuă
Pas 3: Selectează vârful 3, elimină 3 din V0, elimină arcul e2 din E0
D 0:

1
e1
2

156
Grafuri

Pas 4: reia de la pasul 2


Pas 2: od D 0 (2 ) = 0 , continuă
Pas 3: Selectează vârful 2, elimină 2 din V0, elimină arcul e1 din E0

D0: • 1
Pas 4: reia de la pasul 2
Pas 2: od D 0 (1) = 0 , continuă
Pas 3: Selectează vârful 1, elimină 1 din V0
V0=Ø
Pas 4: scrie “DA”, stop.
Algoritmul Marimont poate fi descris în C astfel,

#include<stdio.h>
#include<conio.h>
typedef struct{
int vi,vf;} arc;
int grad_exterior(arc *arce,int na,int v)
{
int od=0;
for(int i=0;i<na;i++)
if(arce[i].vi==v) od++;
return od;
}
void elimina_varf(int *varf,int *nv,int v)
{
int gasit=0;
for(int i=0;(i<*nv)&&!gasit;i++)
if(varf[i]==v){
gasit=1;
for(int j=i+1;j<*nv;j++)
varf[j-1]=varf[j];
}
(*nv)--;
}
void elimina_arce(arc *arce,int *na, int v)
{
for(int i=0;i<*na;)
if((arce[i].vi==v)||(arce[i].vf==v)){
for(int j=i+1;j<*na;j++){
arce[j-1].vi=arce[j].vi;
arce[j-1].vf=arce[j].vf;
}
(*na)--;
}
else i++;
}

157
Programarea calculatoarelor

int ciclic(int *varf,arc *arce,int nv, int na)


{
for(int i=0;i<nv;i++)
if(!grad_exterior(arce,na,varf[i])) return 0;
return 1;
}
int Marimont(int *varf,arc *arce,int nv, int na)
{
while(nv){
printf("\n\nGraful curent\n");
printf("Varfuri:");
for(int i=0;i<nv;i++) printf("%i ",varf[i]);
printf("\nArce:");
for(i=0;i<na;i++) printf("(%i,%i) ",arce[i].vi,arce[i].vf);
getch();
if(ciclic(varf,arce,nv,na)) return 0;
int gasit=0;
for(i=0;(i<nv)&&!gasit;i++)
if(!grad_exterior(arce,na,varf[i])){
gasit=1;
elimina_arce(arce,&na,varf[i]);
elimina_varf(varf,&nv,varf[i]);
}
}
return 1;
}
void main()
{
int n,nv, na;
int vf[20],i,j,a[20][20];
arc arce[100];
clrscr();
printf("Numarul de varfuri");
scanf("%i",&n);
for(i=0;i<n;i++)
vf[i]=i+1;
nv=n;na=0;
printf("Matricea de adiacenta:\n");
for(i=0;i<n;i++)
for(j=0;j<n;j++){
scanf("%i",&a[i][j]);
if(a[i][j]){
arce[na].vi=i+1;
arce[na].vf=j+1;
na++;
}
}

158
Grafuri

if(Marimont(vf,arce,nv,na))
printf("\n\nDigraful este aciclic");
else printf("\n\nDigraful este ciclic");
getch();
}

159
9 Structuri arborescente

Una dintre cele mai studiate clase de grafuri sunt cele de tip arbore. Î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: arbori de sortare şi arbori de
structură.

9.1 Grafuri de tip arbore


9.1.1 Definiţii şi caracterizări ale grafurilor arbori

Structurile cele mai simple şi care apar cel mai frecvent în aplicaţii sunt
cele arborescente (arbori). Grafurile arbori constituie o subclasă a grafurilor
conexe.
Definiţia 9.1.1 Graful G este arbore dacă G este aciclic şi conex.
Definiţia 9.1.2. Fie G=(V,E) graf arbore. Subgraful H=(V1,E1) al lui G este
subarbore al lui G dacă H este graf arbore.

Exemple
9.1.1. Graful
1

2
3 4

5 6
7

160
Structuri arborescente

este arbore, deoarece, orice (i,j) ∈ E , i≠j, există un i-j drum şi graful nu conţine
cicluri.

9.1.2. Graful
1 3

4 2 7
5
7
nu este arbore, deoarece drumul Γ :1,4,6,2,1 este un ciclu.

9.1.3. Graful
1 3 7

4 2
5
9
6
8
nu este arbore, deoarece conţine trei componente conexe: {1,2,3,4,6}, {3} şi {7,8}.

Verificarea proprietăţii unui graf de a fi arbore poate fi realizată prin


intermediul unor algoritmi care să verifice calităţile de conexitate şi respectiv
aciclicitate. De asemenea, verificarea proprietăţii unui graf de a fi arbore poate fi
realizată astfel.
Proprietatea 1. Un graf G=(V,E), cu V = n , E = m este graf arbore dacă
şi numai dacă G este aciclic şi n=m+1.

Exemple
9.1.4. Graful din 9.1.1 este arbore, pentru că este aciclic şi n=7, m=6.
9.1.5. Graful din 9.1.2. nu este arbore pentru că este ciclic.
9.1.6. Graful din exemplul 9.1.3. nu este arbore deoarece este aciclic, dar
n=9, m=6.
Proprietatea 2 Un graf G=(V,E), cu V = n , E = m este graf arbore dacă
şi numai dacă G este conex şi n=m+1.

Exemple
9.1.7. Graful din 9.1.1. este arbore deoarece este conex şi n=m+1.

161
Programarea calculatoarelor

9.1.8. Graful conex din exemplul 9.1.2. nu este arbore pentru că n=6 şi
m=8.
9.1.9. Graful din 9.1.3. nu este conex, deci nu este graf arbore.

Observaţie
Fie G=(V,E) un graf. Următoarele afirmaţii sunt echivalente,
1. G este graf arbore;
2. G este graf conex minimal: oricare ar fi e∈E, prin eliminarea muchiei e
din E, graful rezultat nu este conex;
3. G este graf aciclic maximal: prin adăugarea unei noi muchii în graf
rezultă cel puţin un ciclu.

Definiţia 9.1.3. Se numeşte graf asimetric un digraf D=(V,E) cu


proprietatea că pentru orice u ,v ∈ E dacă uv∈E, atunci vu ∉ E. Digraful D este
simetric dacă ∀u , v ∈ E , uv∈E, dacă şi numai dacă vu∈E.
Definiţia 9.1.4. Fie D=(V,E) digraf netrivial. Graful G=(V,E’), unde
E’={uv/ uv∈E sau vu∈E} se numeşte graf suport al digrafului D.
Definiţia 9.1.5. Un arbore direcţionat este un graf orientat asimetric şi
astfel încât graful suport corespunzător lui este graf arbore.
Definiţia 9.1.6. Arborele direcţionat T=(V,E) este arbore 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 9.1.7. Fie T=(V,E) arbore direcţionat. Arborele T1=(V1,E1) este
subarbore al lui T dacă V1⊆V, E1⊆E şi T1 este arbore direcţionat.

Observaţie Graful suport al unui arbore direcţionat este aciclic, deci,


pentru orice u∈V, u ≠ r, r-u drumul din T este unic. De asemenea, un arbore
direcţionat are cel mult o rădăcină. Rezultă că, 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
9.1.10. Arborele direcţionat
1
3 4

6
5
2

7 8 9 10
este arbore cu rădăcină 1.

162
Structuri arborescente

9.1.11. Arborele direcţionat


1 2

3 7

5 6
nu are rădăcină.
9.1.12. Arborele
1
4

6
5
2

8 10
este un subarbore cu rădăcină 1 al arborelui din 9.1.10.

9.1.2 Reprezentări şi parcurgeri ale arborilor orientaţi

Definiţia 9.1.8. Un arbore orientat este un arbore direcţionat cu rădăcină.


Definiţia 9.1.9. Fie T=(V,E), un arbore orientat cu rădăcină r. Un vârf
v ∈ V este situat pe nivelul i al arborelui T, dacă distanţa de la vârf la rădăcină este
egală cu i. Rădăcina arborelui este considerată de nivel 0.
Deoarece orice arbore orientat este în particular digraf, reprezentarea
arborilor orientaţi poate fi realizată prin utilizarea oricăreia dintre modalităţile
prezentate în §8.1. Datorită caracteristicilor arborilor orientaţi pot fi însă obţinute
reprezentări mai eficiente din punct de vedere al spaţiului de memorie solicitat.
Una dintre modalităţi este reprezentarea de tip FIU-FRATE, care constă în
numerotarea convenţională a vârfurilor grafului şi memorarea, pentru fiecare vârf i
al arborelui, a următoarelor informaţii,
- FIU(i): numărul ataşat primului descendent al vârfului i;
- FRATE(i): numărul ataşat vârfului descendent al tatălui vârfului i şi care
urmează imediat lui i;
- INF(i): informaţia ataşată vârfului i (de obicei valoarea i).
Pentru reprezentarea arborelui sunt reţinute rădăcina şi numărul nodurilor.
Absenţa „fiului”, respectiv a :fratelui” unui vârf este marcată printr-o valoare

163
Programarea calculatoarelor

din afara mulţimii de numere ataşate vârfurilor (de obicei valoarea 0).

Exemplu
9.1.13. Arborele orientat
1

2 3 4

5 6 7 8

9 10 11 12 13 14 15 16
este reprezentat astfel,
N=16, R=1 (rădăcina),
FIU=(2,5,0,8,0,9,0,14,0,0,0,0,0,0,0,0)
FRATE=(0,3,4,0,6,7,0,0,10,11,12,13,0,15,16,0)
O alternativă a reprezentării FIU-FRATE poate fi obţinută prin utilizarea
structurilor de date dinamice. Presupunând că fiecare vârf al arborelui are cel mult
n descendenţi, fiecărui vârf îi este ataşată structura,

identificator vârf vector de legături către descendenţii vârfului


adresă fiu 1 … adresă fiu n

Următoarea sursă C implementează problema construcţiei unui arbore


orientat, reprezentat prin intermediul unei structuri dinamice arborescente.
Numărul maxim de descendenţi ai unui nod este 4. În cazul unui număr mai mare
de descendenţi este preferată în general reprezentarea FIU-FRATE, datorită
dimensiunii spaţiului de memorie ocupat. Afişarea informaţiilor arborelui creat este
realizată prin traversarea în A-preordine (a se vedea paragraful următor).
#include<stdio.h>
#include<conio.h>
#include<alloc.h>
typedef struct nod{
int inf;
struct nod *fiu[4];
} arb, *arbore;
void inserare_tata(arbore *ptata,int k,int info)
{ arbore nou=(arbore)malloc(sizeof(arb));
nou->inf=info;

164
Structuri arborescente

for(int i=0;i<4;i++)nou->fiu[i]=NULL;
(*ptata)->fiu[k]=nou;
}
void inserare(arbore *ppred)
{ int j,info;
arbore *pred;
for(int nr=0;(*ppred)->fiu[nr];nr++){
(*pred)=(*ppred)->fiu[nr];
printf("Numarul de fii ai nodului %i:",(*pred)->inf);
scanf("%i",&j);
for(int k=0;k<j;k++){
scanf("%i",&info);
inserare_tata(pred,k,info);
}
}
for(nr=0;(*ppred)->fiu[nr];nr++)
inserare(&((*ppred)->fiu[nr]));
}
void A_preordine(arbore r)
{
if(r){
printf("%i ",r->inf);
for(int i=0;i<4;i++)
A_preordine(r->fiu[i]);
}
}
void main(){
clrscr();
int n,j,info;
arbore radacina=NULL;
printf("Introduceti informatiile pe niveluri\n");
printf("Introduceti radacina\n");
scanf("%i",&info);
radacina=(arbore)malloc(sizeof(arb));
radacina->inf=info;
for(int i=0;i<4;i++)radacina->fiu[i]=NULL;
printf("Numarul de fii ai nodului %i",radacina->inf);
scanf("%i",&j);
for(int k=0;k<j;k++){
scanf("%i",&info);
inserare_tata(&radacina,k,info);
}
arbore ppred=radacina;
inserare(&ppred);
printf("Parcurgerea A-preordine a arborelui : \n");
A_preordine(radacina);
getch();}

Parcurgerea unui arbore orientat revine la aplicarea sistematică a unei


reguli de vizitare a vârfurilor arborelui. Cele mai utilizate reguli de parcurgere a
arborilor orientaţi sunt A-preordine, A-postordine şi parcurgerea pe niveluri.

165
Programarea calculatoarelor

Parcurgerea în A-preordine
Modalitatea de vizitare a vârfurilor în parcurgerea în A-preordine poate fi
descrisă astfel. Iniţial, rădăcina arborelui este selectată drept vârf curent. Este vizitat
vârful curent şi sunt identificaţi descendenţii lui. Se aplică aceeaşi regulă de vizitare
pentru arborii avînd ca rădăcini descendenţii vârfului curent, arborii fiind vizitaţi în
ordinea precizată prin numerele ataşate vârfurilor rădăcină corespunzătoare.

Exemplu
9.1.14. Pentru arborele orientat din exemplul 9.1.13., prin aplicarea
parcurgerii în A-preordine, rezultă: 1,2,5,6,9,10,11,12,13,7,3,4,8,14,15,16.

În reprezentarea FIU-FRATE, implementarea parcurgerii în A-preordine


este realizată prin următoarea funcţie recursivă, cu parametru de intrare rădăcina
arborelui curent.
void A_preordine (nod R)
{
if (R){
vizit (R);
A_preordine(FIU[R]);
A_preordine(FRATE[R]);
}
}

În sursa prezentată în paragraful precedent, funcţia A-preordine


implementează acest tip de traversare în cazul arborilor orientaţi reprezentaţi prin
intermediul structurilor arborescente.

Parcurgerea A-postordine
Regula de parcurgerea în A-postordine este asemănătoare traversării A-
preordine, singura diferenţă fiind aceea că, în acest tip de traversare, rădăcina
fiecărui arbore este vizitată după ce au fost vizitate toate celelalte vârfuri ale
arborelui.
Exemplu
9.1.15. Pentru arborele orientat din exemplul 9.1.13. ordinea de vizitare a
vârfurilor este: 5,9,10,11,12,13,6,7,2,3,14,15,16,8,4,1.
Pentru arbori reprezentaţi prin structuri dinamice de date, implementarea
parcurgerii în A-postordine poate fi obţinută pe baza următoarei funcţii recursive.
Parametrul de intrare al funcţiei A_postordine reprezintă rădăcina arborelui curent
în momentul apelului.

void A_postordine (nod R)


{
if (R) {
for(i=0;i<n;i++) A_postordine(R->leg[i]);
vizit (R);
}
}

166
Structuri arborescente

Observaţie
Parcurgerile în A-preordine şi A-postordine sunt variante de parcurgeri în
adâncime (variante ale metodei DF). Ambele metode consideră prioritare vârfurile
aflate la distanţă maximă faţă de rădăcina arborelui iniţial.

Parcurgerea pe niveluri
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
9.1.16. Pentru arborele definit în exemplul 9.1.13., prin aplicarea
parcurgerii pe niveluri, rezultă următoarea ordine de vizitare a nodurilor, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16.

Ca şi în cazul metodei BF, implementarea parcurgerii pe niveluri este


bazată pe utilizarea unei structuri de coadă C. La momentul iniţial rădăcina
arborelui este inserată în C. Atîta timp cît timp coada este nevidă, este preluat cu
ştergere un vârf din C, este vizitat şi sunt introduşi în coadă descendenţii săi.
Calculul este încheiat cînd C=Ø.
În cazul reprezentării FIU-FRATE a arborelui de traversat, parcurgerea pe
niveluri poate fi implementată prin următoarea funcţie.

void parcurgere_pe_niveluri(nod R,int FIU[],int FRATE[],int n)


{
ptcoada C=NULL;push(C,R);
while (C) {
pop(C,v); VIZIT(v);
v=FIU[v];
while (v){
push(C,v); v=FRATE[v];
}
}
}

Observaţie
Funcţiile push şi pop implementează inserarea unuei celule în coadă,
respectiv extragerea unui element al cozii.

Exemplu
9.1.17. Pentru arborele de la exemplul 9.1.13., evoluţia algoritmului este,

C
t
t=1 1 8
t=2 2 3 4
t=3 3 4 5 6 7
t=4 4 5 6 7
t=5 5 6 7 8

167
Programarea calculatoarelor

C
t
t=6 6 7 8
t=7 7 8 9 10 11 12 13
t=8 8 9 10 11 12 13
t=9 9 10 11 12 13 14 15 16
t=10 10 11 12 13 14 15 16
t=11 11 12 13 14 15 16
t=12 12 13 14 15 16
t=13 13 14 15 16
t=14 14 15 16
t=15 15 16
t=16 15
t=17

deci vârfurile sunt vizitate în ordinea: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16.

Observaţie
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 funcţiilor recursive frati şi parc. Coada C este o variabilă globală
şi este iniţializată cu rădăcina arborelui. Parcurgerea este realizată prin apelul
parc(C).

void frati(v)
{if (v){push(C,v);
fraţi(FRATE[v]);
}
}

void parc()
{if (C){pop(C,v);VIZIT(v);
frati(FIU[v]); parc();
}
}

9.1.3 Arbori parţiali. Algoritmul Kruskal

Definiţia 9.1.10. Fie G un graf. Subgraful parţial H este un arbore parţial


al lui G dacă H este graf arbore.
Definiţia 9.1.11. Fie G=(V,E,w) un graf ponderat conex.Dacă T=(V,E0) este
un arbore parţial al grafului G’=(V,E), ponderea arborelui T, notată W(T), este
definită prin W(T)= ∑ w( e ) .
e∈E0

168
Structuri arborescente

Exemplu
9.1.18. Pentru graful ponderat
1
4 3
2

2 6 8 5

2 9
1 12

4 3

T este un arbore parţial de pondere 32.


1

4 2 3

2 6 5

8 9

4 3

Definiţia 9.1.12. Arborele parţial T0∈T(G) este arbore parţial minim pentru
G dacă W(T0)=min{W(T); T∈T(G)}, unde T(G) este mulţimea arborilor parţiali
corespunzători grafului G.

Observaţie
Dacă G este graf finit, atunci T(G) este o mulţime finită, deci orice graf
finit ponderat şi conex are cel puţin un arbore parţial minim.
În continuare este prezentat algoritmul Kruskal pentru determinarea unui
arbore parţial minim al unui graf ponderat conex G=(V,E,w).
Pas 1: i=1; E0=∅
Pas 2: Determină 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}
Pas 3: i=i+1 şi reia pasul 2.

169
Programarea calculatoarelor

Arborele parţial de cost minim al grafului G este (V,Ei-1).


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 de muchii din arborele
parţial minim a cărui construcţie este dorită. În varianta prezentată în continuare
muchiile selectate sunt afişate.
Verificarea condiţiei ca muchia selectată să nu formeze niciun ciclu cu
muchiile selectate la etapele precedente este realizată prin utilizarea un vector
TATA, definit astfel. Pentru fiecare vârf i (vârfurile grafului fiind numerotate de la
1 la n, unde n este numărul de noduri ale 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 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.
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ă.
Adăugarea muchiei e selectate este realizată prin reunirea arborilor din care fac
parte v1 şi v2 de rădăcini r1, respectiv r2, astfel: dacă TATA[r1]<TATA[r2], atunci
arborele rezultat prin reunirea celor doi arbori are ca rădăcină vârful r1, iar vârful
r2 devine fiu al lui r1. Altfel, rădăcina arborelui rezultat prin reunire fiind r2, iar r1
devenind fiu al rădăcinii. Calculul se încheie după ce a fost adăugată şi cea de-a
(n-1)-a muchie.
Algoritmul Kruskall poate fi implementat prin următoarea sursă C:

#include<stdio.h>
#include<conio.h>
int radacina(int v,int *tata)
{ int u=v;
while(tata[u]>=0) u=tata[u];
return u; }
int kruskal(int a[][3],int nm, int nv)
{
int tata[50],i,j;
int c=0;
for(i=0;i<nv;i++)tata[i]=-1;
for(j=i=0;i<nv-1;j++){
int v1=a[j][0]; int v2=a[j][1];
int k=radacina(v2,tata);int p=radacina(v1,tata);
if(k-p){
if(tata[k]<tata[p]){
tata[k]+=tata[p];tata[p]=k;
}
else{
tata[p]+=tata[k];tata[k]=p;
}

170
Structuri arborescente

c+=a[j][2];printf("%i -> %i cost %i\n",v1+1,v2+1,a[j][2]);


i++;
}
}
return c;
}
void main()
{
clrscr();
int nv,nm, a[100][3];
printf("Numarul de varfuri:");scanf("%i",&nv);
printf("Numarul de muchii");scanf("%i",&nm);
printf("Matricea de reprezentare\n");
for(int i=0;i<nm;i++)
for(int j=0;j<3;j++)
scanf("%i",&a[i][j]);
for(i=0;i<nm;i++)
for(int j=0;j<2;j++)a[i][j]--;
printf("Arborele de cost minim: \n");
int cost=kruskal(a,nm,nv);
printf("\ncu costul%i",cost);
getch();
}

Exemplu
9.1.19. Evoluţia determinată de program pentru graful
1 ⎛2 3 1⎞
⎜ ⎟
⎜2 4 2⎟
⎜1 6 2⎟
4 2 3 ⎜ ⎟
⎜1 5 3⎟
2 6 8 5 A = ⎜3 ⎟
⎜ 4 4⎟
⎜1 2 4⎟
9 ⎜ ⎟
2 8 1 ⎜4 6 8⎟
⎜5 6 8⎟
⎜⎜ ⎟
4
⎝3 6 9 ⎟⎠
4 3

este:

i, j după cea de-a t-a iteraţie muchia selectată TATA Costul


t=0 (-1,-1,-1,-1,-1,-1)
t=1,i=0,j=0 (2,3) (-1,-2,2,-1,-1,-1) 1
t=2,i=1,j=1 (2,4) (-1,-3,2,2,-1,-1) 2
t=3,i=2,j=2 (1,6) (-2,-3,2,2,-1,1) 2
t=4,i=3,j=3 (1,5) (-3,-3,2,2,1,1) 3
t=5,i=4,j=4 (-3,-3,2,2,1,1)
t=6,i=4,j=5 (1,2) (-5,1,1,2,1,1) 4
MUCHIILE ARBORELUI MINIM: {(2,3),(2,4),(1,6),(1,5),(1,2)} COSTUL: 12

171
Programarea calculatoarelor

9.2 Arbori binari


9.2.1 Reprezentarea arborilor binari. Modalităţi de parcurgere

Definiţia 9.2.1 Un arbore binar este un arbore orientat cu proprietatea că


pentru orice vârf v, od(v)≤2. Dacă 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 9.2.2 Se numeşte nod terminal orice vârf v al arborelui cu
od(v)=0. În caz contrar nodul v este neterminal.
Reprezentarea unui arbore binar este realizată printr-o structură
arborescentă. Pentru fiecare nod N al arborelui binar sunt memorate informaţia
asociată lui N şi legăturile către descendenţii lui. Absenţa unui descendent este
reprezentată prin NULL.
identificator legătură fiu legătură fiu
nod stâng drept
Definiţia 9.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.
Exemplu
9.2.1 Pentru arborele binar,
1

2 3

4 5 6 7

8 9 10
subarborii rădăcinii
2 3

6 7
4 5

8 9 10
Subarbore stâng Subarbore drept
sunt:

172
Structuri arborescente

În plus faţă de metodele A-preordine, A-postordine şi pe niveluri,


parcurgerile în preordine (RSD), inordine (SRD) şi respectiv postordine (SDR) sunt
special considerate pentru arbori binari şi au multiple aplicaţii. Regula de vizitare
pentru aceste tipuri de parcurgere revine la parcurgerea subarborelui stâng şi
parcurgerea subarborelui drept corespunzători vârfului curent. La momentul iniţial
vârful curent este rădăcina arborelui. Diferenţa dintre cele trei tipuri de parcurgere
este dată de momentul în care devine 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 este vârf curent; în parcurgerea SRD,
vizitarea vârfului curent R este efectuată după ce a fost parcurs subarborele stâng al
lui R, respectiv în parcurgerea SDR vizitarea fiecărui vârf este efectuată după ce
au fost parcurşi subarborii aferenţi lui.
Exemplu
9.2.2 Pentru arborele de la exemplul 9.2.1., secvenţele de vârfuri rezultate
prin aplicarea parcurgerilor RSD, SRD, SDR sunt:
- preordine: 1,2,4,8,5,3,6,9,10,7
- inordine: 4,8,2,5,1,9,6,10,3,7
- postordine: 8,4,5,2,9,10,6,7,3,1.

9.2.2 Arbori de sortare

Definiţia 9.2.4 Un arbore de sortare este un arbore binar cu următoarele


proprietăţi,
- 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 şi j daca i≠j atunci INF(i)≠INF(j).
Exemplu
9.2.3 Arborele binar
50

30 70

10 40 90

20 80

este arbore de sortare.

173
Programarea calculatoarelor

Operaţiile primitive asupra arborilor de sortare sunt inserarea unui nod,


ştergerea unui nod şi parcurgerea arborelui (în preordine, inordine sau postordine).
Inserarea şi ştergerea de noduri aplicate unui arbore de sortare trebuie realizate
astfel încât arborele rezultat să fie de asemenea arbore de sortare.

Observaţie
Parcurgerea în inordine a unui arbore de sortare determină obţinerea
secvenţei informaţiilor asociate vârfurilor arborelui în ordine crescătoare.

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 următoarelor operaţii: vârful
curent v la momentul iniţial este rădăcina arborelui; dacă arborele de rădăcină v
este vid, este generat arborele cu un singur nod, cu informaţia ataşată nr; altfel:
- dacă informaţia ataşată nodului v este mai mare decât nr, atunci vârf
curent devine fiul stânga al lui v;
- dacă informaţia ataşată nodului v este egală cu nr, atunci stop;
- dacă informaţia ataşată nodului v este mai mică decât nr, atunci vârf
curent devine fiul dreapta al lui v.

Exemplu
9.2.4 Aplicarea algoritmul descris pentru inserarea informaţiei 55 în
arborele de sortare din exemplul 9.2.3 determină următoarele operaţii,
INF(v)=50; 50<55, inserează în subarborele cu rădăcina avînd informaţia
ataşată 70.
INF(v)=70; 70>55, inserează în subarborele stâng cu rădăcina NULL.
Este creat nodul cu informaţie 55, fiu stâng al nodului de informaţie 70.
Arborele rezultat este
50

30 70

10 40 55 90

20 80

Ş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 şi poate fi descris astfel. Vârful curent v la momentul
iniţial este rădăcina arborelui.

174
Structuri arborescente

1. dacă arborele este vid atunci stop;


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, 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;
c2) dacă subarborele stâng este nevid atunci se determină cel mai mare
element din subarborele stâng;
c2.1) dacă fiul stânga al lui v nu are subarbore drept, atunci informaţia
ataşată fiului stânga se transferă în vârful curent, iar fiul stânga este
înlocuit cu fiul său stânga ş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.

Exemplu
9.2.5 Ştergerea informaţiei 70 din arborele de sortare din exemplul 9.2.4.
este realizată astfel:
70>50, decide ştergerea din subarborele drept
70=70, decide ştergerea din arborele curent: rădăcina etichetată cu 70;
există subarbore stâng iar acesta nu are subarbore drept- nodul cu informaţie
70 este etichetat cu 55, iar p este înlocuit cu subarborele său stâng (vid). Arborele
rezultat

50

30 55

10 40 90

20 80
este arbore de sortare.

175
Programarea calculatoarelor

Observaţie
Punctul c) de la pasul 2 al algoritmului de eliminare a unei informaţii
dintr-un arbore de sortare poate fi înlocuit cu:
c) dacă INF(v)=nr atunci:
c1) dacă subarborele drept este vid, 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;
c2) dacă subarborele drept este nevid atunci se determină cel mai mic
element din subarborele drept, altfel:
c2.1.) dacă fiul dreapta al lui v nu are subarbore stâng, atunci
informaţia ataşată fiului dreapta este transferată în vârful curent, iar
fiul dreapta este înlocuit cu fiul său dreapta ş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.
În următoarea sursă C sunt implementaţi algoritmii de adăugare şi ştergere
în arbori de sortare.
#include<stdio.h>
#include<conio.h>
#include<alloc.h>

typedef struct nod{


int inf;
struct nod *l,*r;
} arb, *arbore;
void inserare(arbore *radacina,int info)
{
if(*radacina==NULL){
arbore nou;
nou=(arbore)malloc(sizeof(arb));
nou->inf=info;
nou->l=nou->r=NULL;
*radacina=nou;
}
else if((*radacina)->inf>info)
inserare(&((*radacina)->l),info);
else if((*radacina)->inf<info)
inserare(&((*radacina)->r),info);
}
int extragere(arbore *radacina,int info)
{
if(*radacina==NULL) return 0;
else if((*radacina)->inf>info)
return extragere(&((*radacina)->l),info);
else if((*radacina)->inf<info)
return extragere(&((*radacina)->r),info);

176
Structuri arborescente

else{
if((*radacina)->l==NULL){
arbore aux=*radacina;
*radacina=(*radacina)->r;
free(aux);
}
else{
arbore p,p1;
for(p=(*radacina)->l;p->r;p1=p,p=p->r);
if(((*radacina)->l)->r==NULL){
(*radacina)->inf=p->inf;
(*radacina)->l=p->l;
free(p);
}
else{
(*radacina)->inf=p->inf;
arbore aux=p;
p1->r=p->l;
free(aux);
}
}
return 1;
}
}
void srd(arbore radacina)
{
if(radacina){
srd(radacina->l);
printf("%i ",radacina->inf);
srd(radacina->r);
}
}
void main()
{
clrscr();
int n,info;
arbore radacina=NULL;
printf("Numarul de noduri:");
scanf("%i",&n);
printf("Introduceti informatiile\n");
for(int i=0;i<n;i++){
scanf("%i",&info);
inserare(&radacina,info);
}
printf("Parcurgerea SRD a arborelui de sortare: \n");
srd(radacina);
printf("\nInformatia nodului de extras:");
scanf("%i",&info);
if(extragere(&radacina,info)){
printf("\nArborele rezultat in parcurgere SRD\n");
srd(radacina);
}
else printf("\nInformatia nu este in arbore");
getch();
}

177
Programarea calculatoarelor

9.2.3 Arbori de structură

Expresiile aritmetice în care intervin numai operatori binari pot fi reprezentate


prin intermediul arborilor binari în care fiecare nod neterminal are doi fii.
Definiţia 9.2.5 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.
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
9.2.6 Pentru expresia matematică (a+b)*(c-d)+e/g, arborele de structură
corespunzător este
+

* /

+ - e g

a b c d

Construcţia arborelui de structură pentru o expresie s este realizată în două


etape:
1. ataşarea de priorităţi operatorilor şi operanzilor; priorităţile ataşate
permit eliminarea parantezelor fără ca semnificaţia expresiei să se
modifice;
2. construcţia propriu-zisă.

Prima etapă este realizată astfel:


- prioritatea iniţială a operatorilor ‘+’,’-‘ este 1 (dacă expresia nu conţine
paranteze atunci în construcţie aceşti operatori 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ă;

178
Structuri arborescente

- prioritatea ataşată fiecărui operand este MAXINT.


După stabilirea sistemului de priorităţi sunt eliminate parantezele din
expresie, ordinea de efectuare a operaţiilor în cadrul expresiei fiind indicată de
vectorul de priorităţi ataşat.
Construcţia arborelui de structură pe baza expresiei s din care au fost
eliminate parantezele şi a vectorului de priorităţi, poate fi realizată recursiv în
modul următor (la momentul iniţial expresia curentă este expresia dată):
- 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, atunci r->fius=r->fiud=NULL;
- altfel, se consideră subexpresiile s1 şi s2, constînd din simbolurile de
pe poziţiile 0 pînă la i-1 şi respectiv i+1 pînă la lungimea şirului s.;
arborii de structură corespunzători subexpresiilor s1 şi s2 se ataşează
ca subarbore stâng, respectiv subarbore drept vârfului r.

Exemplu
9.2.7 Etapele calculului sistemului de priorităţi şi al arborelui de structură
pentru expresia de la exemplul 9.2.6 pot fi descrise astfel,

Dim vectorul prioritate


1 (MAXINT)
2 (MAXINT,11)
3 (MAXINT,11,MAXINT)
3 (MAXINT,11,MAXINT)
4 (MAXINT,11,MAXINT,10)
4 (MAXINT,11,MAXINT,10)
5 (MAXINT,11,MAXINT,10,MAXINT)
6 (MAXINT,11,MAXINT,10,MAXINT,11)
7 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT)
7 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT)
8 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1)
9 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT)
10 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT,10)
11 (MAXINT,11,MAXINT,10,MAXINT,11,MAXINT,1,MAXINT,10,MAXINT)
După eliminarea parantezelor, expresia rezultată este s=a+b*c-d+e/g.
Arborele de structură este construit astfel:

+
* în construcţie

în construcţie în construcţie în construcţie în construcţie

179
Programarea calculatoarelor

+ +

în construcţie * în construcţie
*

+ în construcţie + în construcţie

în construcţie în construcţie a în construcţie


+ +

* în construcţie * în construcţie

+ în construcţie + -

a b a b în construcţie în construcţie

+ +

* în construcţie * în construcţie

+ - + -

a b c în construcţie a b c d

180
Structuri arborescente

* /

+ - în construcţie în construcţie

a b c d

* /

+ - e în construcţie

a b c d
+

* /

+ - e g

a b c d

Observaţie
Construcţia arborelui de structură poate fi realizată în ipoteza în care
expresia este corectă.

Definiţia 9.2.6 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ă.

181
Programarea calculatoarelor

Exemplu
9.2.8 Pentru expresia considerată la exemplul 9.2.7, forma poloneză directă
este +*+ab-cd/eg. Forma poloneză inversă a expresiei date este ab+cd-*eg/+.

Observaţie
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 şi anume î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).

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 astfel. 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ă.

Exemplu
9.2.9 Prin aplicarea metodei de evaluare descrise pentru a=3, b=2, c=5,
d=2, e=6 şi g=2, obţinem:
18

15 3

5 3 6 2

3 2 5 2

182
Structuri arborescente

Construcţia arborelui de structură asociat unei expresii şi evaluarea


expresiei pentru valori date ale operanzilor pot fi implementate prin intermediul
următoarei surse C.

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include<values.h>
#include<string.h>
#include<math.h>
typedef struct nod{
char inf;
float v;
struct nod *l,*r;
} arb, *arbore;
void prioritati(char *s, int *prioritate)
{
int i,j,dim;
//stabilirea prioritatilor
for(i=j=dim=0;i<strlen(s);i++)
switch(s[i]){
case ')':j-=10;break;
case '(':j+=10;break;
case '+':{prioritate[dim]=j+1;dim++;break;}
case '-':{prioritate[dim]=j+1;dim++;break;}
case '*':{prioritate[dim]=j+10;dim++;break;}
case '/':{prioritate[dim]=j+10;dim++;break;}
default:{prioritate[dim]=MAXINT;dim++;break;}
}
//eliminarea parantezelor
for(i=0;i<strlen(s);)
if((s[i]==')')||(s[i]=='(')){
for(j=i+1;j<strlen(s);j++)s[j-1]=s[j];
s[strlen(s)-1]='\0';}
else i++;
}
void cr_arb_str(arbore *rad, unsigned p, unsigned u, char *s,int
*pri)
{
int min=pri[p];
int poz=p;
for(int i=p+1;i<=u;i++)
if(min>pri[i]){min=pri[i];poz=i;}
(*rad)=(arbore)malloc(sizeof(arb));
(*rad)->inf=s[poz];
if(p==u)
(*rad)->l=(*rad)->r=NULL;
else{
cr_arb_str(&((*rad)->l),p,poz-1,s,pri);
cr_arb_str(&((*rad)->r),poz+1,u,s,pri);
}
}

183
Programarea calculatoarelor

void forma_poloneza(arbore rad)


{
if(rad){
printf("%c",rad->inf);
forma_poloneza(rad->l);
forma_poloneza(rad->r);
}
}

float eval(arbore rad)


{
char s[1];
if(rad){
if((rad->r==rad->l)&&(rad->l==NULL))return rad->v;
else{
switch (rad->inf){
case '+':rad->v=eval(rad->l)+eval(rad->r);break;
case '-':rad->v=eval(rad->l)-eval(rad->r);break;
case '*':rad->v=eval(rad->l)*eval(rad->r);break;
case '/':rad->v=eval(rad->l)/eval(rad->r);break;
}
return rad->v;
}
}
}
void atribuie_arbore(arbore rad)
{
if(rad){
if((rad->r==rad->l)&&(rad->l==NULL)){
printf("%c =",rad->inf);
float t;
scanf("%f",&t);
rad->v=t;
}
else
{atribuie_arbore(rad->l);
atribuie_arbore(rad->r);
}
}
}

void main()
{
clrscr();
char s[100];
int p[100];
arbore radacina=NULL;
printf("Expresia:");
scanf("%s",&s);
prioritati(s,p);

184
Structuri arborescente

int n=strlen(s);
cr_arb_str(&radacina,0,n-1,s,p);
printf("\nForma poloneza inversa ");
forma_poloneza(radacina);
printf("\n Valori pentru varabile\n");
atribuie_arbore(radacina);
printf("\nEvaluarea: %7.3f",eval(radacina));
getch();
}

185
10 Elemente de programare
orientată obiect

Programarea orientată obiect (Object Oriented Programming - OOP)


reprezintă o tehnică ce s-a impus în anii ’90, dovedindu-se benefică pentru
realizarea sistemelor software de mare complexitate. Noţiunea de obiect datează
din anii ’60, odată 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. Recent au fost dezvoltate şi alte limbaje orientate obiect,
fie pentru programare generală, fie pentru realizarea de scripturi – Java, Delphi,
C++, Visual Basic .NET, C#, Python, Ruby. Unele dintre ele oferă în continuare şi
posibilitatea programări procedurale (Delphi, C++). Toate limbajele folosite în
prezent oferă şi facilităţi de programare orientată obiect – ADA, Fortran, Cobol,
PHP etc. Î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.
Acest capitol prezintă o introducere în lucrul orientat obiect în limbajul
C++, fără a acoperi toată problematica specifică.

10.1 Modelul de date orientat obiect


OOP reprezintă o abordare cu totul diferită faţă de programarea
procedurală, 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 între acestea, ca elemente prin care se modelează
obiectele lumii reale. Se poate afirma, într-o primă analiză, că OOP organizează un

186
Elemente de programare orientată obiect

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: abstractizare, obiect, atribut, metodă,
clasă, spaţiu propriu, spaţiu extins, încapsulare, moştenire şi polimorfism.

Abstractizarea constituie procesul de simplificare a realităţii prin reţinerea


caracteristicilor şi comportamentelor esenţiale şi constituirea lor într-un model
adecvat rezolvării problemelor.

Obiectul este un model 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ă reacţii specifice în relaţiile cu alte entităţi din
mediul său de existenţă. Ca model, un obiect este o unitate individualizabilă prin
nume, care conţine o mulţime de date şi funcţii. Datele descriu proprietăţile şi
nivelul acestora, iar funcţiile definesc comportamentul.
Având în vedere proprietăţile comune şi comportamentul similar al
entităţilor pe care le modelează, obiectele pot fi clasificate în mulţimi. O mulţime
de obiecte de acelaşi fel constituie o clasă de obiecte, descrisă prin modelul comun
al obiectelor sale.
De exemplu, în figura 10.1, numerele complexe, ca perechi de numere
reale de forma (parte reală, parte imaginară) pot fi descrise printr-un model comun,
denumit ClasaComplex. 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 complex poate da naştere modulului şi opusului său, două numere complexe
pot produce un alt număr complex ca sumă, produs etc.
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.

Figura 10.1 Clasă şi obiecte – mulţimea numerelor complexe

187
Programarea calculatoarelor

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, reale, caractere etc.). Setul de valori ale atributelor unui obiect la
un moment dat formează starea curentă a obiectului respectiv. 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, astfel: nume_obiect.nume_membru. În figura 10.1, a.P_reală referă
valoarea 1.0, iar a.Modul referă metoda Modul a obiectului a pentru a produce
obiectul rezultat.
Aşa cum sugerează figura 10.1, fiecare obiect trebuie să conţină valorile
atributelor sale, deoarece ele definesc starea obiectului respectiv. Spaţiul de
memorie ocupat de atributele unui obiect se numeşte spaţiu propriu al obiectului.
În multe cazuri, între atribute se află pointeri care indică anumite zone de memorie
alocate dinamic pentru obiect (de exemplu, clasa listă are ca membru atributul cap
care conţine adresa primului nod al unei liste dinamice simplu înlănţuite). Acest
spaţiu alocat dinamic aparţine tot obiectului, dar el se numeşte spaţiu extins al
obiectului. Gestiunea acestui spaţiu extins trebuie asigurată de metodele clasei.
Metodele, care descriu acţiuni identice pentru toate obiectele clasei, sunt
memorate o singură dată, într-o zonă comună tuturor obiectelor clasei. Întrucât
metodele descriu comportamentele obiectelor, ele nu pot fi apelate independent, ci
numai în legătură cu un anumit obiect. Despre o metodă apelată pentru un anumit
obiect se spune că se execută în contextul obiectului respectiv, iar acesta este numit
obiect curent. Apelarea metodei este considerată ca trimitere de mesaj către
obiectul curent, iar execuţia metodei reprezintă răspunsul (reacţia) obiectului curent
la mesajul primit.
Faptul că o metodă se execută în contextul obiectului curent înseamnă că
are, în mod implicit, acces la toate atributele şi metodele obiectului. Acestea nu
trebuie să apară ca parametri ai metodei. Pentru a utiliza alte obiecte, din aceeaşi
clasă sau din clase diferite, metoda trebuie să aibă parametri 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 funcţiile Conjugat, Suma şi Modul, 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 atributele obiectului curent se distinge de celelalte prin lipsa calificării.
Descrierea în pseudocod a metodelor Conjugat, Suma şi Modul din clasa
CComplex (figura 10.1) poate fi făcută astfel:
void Conjugat(b);
begin b.p_reala:=p_reala;
b.p_imaginara:=-p_imaginara;
end;

188
Elemente de programare orientată obiect

void Suma(b,c);
begin
c.p_reala:=p_reala+b.p_reala;
c.p_imaginara:=-p_imaginara+b.p_imaginara;
end;

float Modul();
begin
Modul=sqrt(p_reala*p_reala+p_imaginara*p_imaginara);
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.2 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).

Figura 10.2 Atribute de tip clasă

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ă (sau
mai multe) constructor şi o metodă destructor. Un constructor este o metodă care
creează un 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.
Î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

189
Programarea calculatoarelor

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, care au ca unic rol accesul la atribute.
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 şi a funcţionalităţii interne a
obiectului.
De exemplu, o stivă, ca tip de dată poate fi descrisă de o clasă Stiva î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.3)

Stiva

Cap: Nod
Partea privată
Contor: Integer

Push ( )
Pop ( )
Partea publică (Interfaţa)
Top ( )
Empty ( )

Figura 10.3 Interfaţa obiectelor

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ţionalitatea obiectelor în condiţii neschimbate de apelare.

Moştenirea reprezintă o relaţie între clase şi este, probabil, elementul


definitoriu al OOP. Relaţia permite constituirea unei noi clase, numită derivată
(sau fiu) pornind de la clase existente, denumite de bază (sau părinte). Dacă în
procesul de construire participă o singură clasă de bază, moştenirea este simplă,
altfel este multiplă.

190
Elemente de programare orientată obiect

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
nou definite.
În figura 10.4, clasa Cerc moşteneşte clasa Punct, 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 centrul cercului şi un alt punct, primit ca parametru. Funcţia Arie calculează
aria din interiorul cercului, fiind nou definită. Funcţia Desenează este redeclarată
de clasa Cerc, lucru impus de codul diferit pe care trebuie să-l aibă pentru
desenarea obiectelor din această clasă (cerc sau altă figură).

Punct
X: int
Y:int X= 100
Desenează() Y= 100
Distanţa(p: punct): float

Cerc
Raza: int
X= 200
Arie():float
Y= 200
Desenează()
Raza= 50
Distanţa(p: punct): float

Figura 10.4 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ă, 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ă, la care se adaugă elemente funcţionale noi;
• moştenirea este mecanismul prin care se asigură reutilizarea codului
sporind productivitatea muncii de programare.

191
Programarea calculatoarelor

Coborând (de obicei, în reprezentările grafice ale arborilor de clase,


rădăcina se află în partea superioară) în arborele ierarhic al claselor de la rădăcină
către frunze, se poate spune că întâlnim clase din ce în ce mai specializate. Prin
moştenire se realizează o specializare a claselor. În sens invers, de la frunză către
rădăcină, clasele sunt din ce în ce mai generale, avem o relaţie de generalizare.
Clasa aflată la baza ierarhiei este cea mai generală.
Limbajele de programare orientate obiect au implementate ierarhii standard
extinse de clase, care corespund necesităţilor generale ale programării. Utilizatorii
pot deriva clase noi din cele standard.

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). El ş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 funcţie 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 funcţii care au
toate acelaşi nume, dar se disting prin numărul şi/sau tipul parametrilor. Acest
polimorfism este denumit şi supraîncărcare, având în vedere semantica specifică
fiecărei funcţii î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 trimite un parametru de tip A unei
funcţii care aşteaptă un parametru de tip B. Astfel, un singur subprogram defineşte
funcţional o familie de funcţii pentru toate tipurile inferioare celor declarate ca
parametri. Un exemplu clasic este cazul tipului int, inferior tipului float în toate
operaţiile de calcul.
Polimorfism parametric constă în definirea unui model de procedură
pentru care chiar tipurile sunt parametri. Acest polimorfism, 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.
• 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

192
Elemente de programare orientată obiect

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
supraîncărcare, ci redefinire, după cum s-a precizat mai sus.
Polimorfimsul 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 o relaţie
de ordine parţială, astfel încât dacă 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.
• Legare 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.
Fie o clasă A şi o clasă D, unde D este derivată din A. Fie o metodă din
clasa A, numită calculează, care este redefinită în clasa derivată, D. Atunci cînd
metoda este apelată în contextul unui obiect static, compilatorul poate determina
tipul acelui obiect (ca fiind parte a clasei A sau D). Astfel, el va şti ce metodă să
apeleze (a clasei de bază sau cea redefinită, a clasei derivate). În acest caz are loc o
legare statică a metodelor (decizia este luată în momentul compilării).
Fie un pointer p, definit ca pointer spre clasa A. Datorită polimorfismului,
î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 acest context,
p poate primi ca valoare, în timpul execuţiei programului, adresa unui obiect din
clasa A sau din clasa D. Nu se poate şti la momentul compilării ce se va întâmpla în
timpul execuţiei programului, ca urmare nu se poate determina dacă, în contextul
dat, trebuie apelată metoda clasei de bază sau metoda clasei derivate. De aceea, în
locul din program în care este apelată metoda, compilatorul adaugă o secvenţă de
cod care, la momentul execuţiei, va verifica tipul efectiv al obiectului şi, după caz,
va realiza apelarea metodei adecvate. În acest caz are loc legarea dinamică a
metodelor (sau la momentul execuţiei). Legarea dinamică este evident 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.

193
Programarea calculatoarelor

10.2 Definirea claselor


Definiţia unei clase este asemănătoare cu definiţia unui articol, însă în
locul cuvântului rezervat struct se foloseşte cuvântul class:
class nume { descriere membri; };

Membrii unei clase pot fi atribute sau metode. Atributele sunt descrise
asemănător declaraţiilor de variabile independente (şi asemănător câmpurilor unui
articol – struct), specificând tipul şi numele atributului respectiv. Membrii unei
clase pot fi de orice tip, mai puţin de acelaşi tip cu clasa descrisă (dar pot fi pointeri
către clasa descrisă).
Metodele sunt descrise asemănător funcţiilor independente. Ele pot fi
descrise integral în interiorul clasei (descriind antetul şi corpul lor) sau specificând
în interiorul clasei doar prototipul funcţiei, corpul urmând să fie descris ulterior, în
afara clasei. Este preferată a doua variantă, deoarece descrierea clasei este mai
compactă decât în primul caz. Atunci când se descrie ulterior corpul unei metode,
pentru a specifica apartenenţa sa la clasa respectivă, numele metodei este prefixat
cu numele clasei din care face parte, folosind operatorul de rezoluţie (::), astfel:
tip_rezultat nume_clasă::nume_metodă(lista parametrilor)
corp metodă

Mai mult, funcţiile care sunt integral descrise în interiorul clasei sunt
considerate funcţii inline1, de aceea ele trebuie să fie simple. Pentru funcţiile mai
complexe, întotdeauna se recomandă să fie descrise folosind a doua variantă.

Întrucât o clasă este un tip de dată, declararea unui obiect se face


asemănător oricărei declaraţii de dată:
nume_clasă nume_obiect;

Atunci când se doreşte lucrul cu obiecte dinamice, se poate declara o variabilă de


tip pointer către clasă: nume_clasă* p_obiect;
Declararea unui obiect mai este numită şi instanţierea clasei, în sensul că
se creează o instanţă a acelei clase, o entitate concretă din mulţimea descrisă de
clasa respectivă.

1
Apelul funcţiilor inline nu produce un salt în segmentul de cod către codul executabil al funcţiei, aşa
cum se întâmplă în cazul funcţiilor obişnuite. Pentru aceste funcţii, compilatorul inserează în
program, în locul apelului, secvenţa de cod corespunzătoare corpului funcţiei, înlocuind parametrii
formali cu valorile actuale. Funcţiile inline au un comportament asemănător macrodefiniţiilor.

194
Elemente de programare orientată obiect

Exemplu
Definirea clasei Complex, care implementează entitatea matematică număr
complex. Clasa are atributele p_reala şi p_imaginara şi o metodă pentru afişarea
valorii obiectului – afiseaza.
class Complex { float p_reala,p_imaginara;
void Afiseaza();
};

void Complex::Afiseaza()
{ printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-',
p_imaginara>=0?p_imaginara:-p_imaginara);
}

Complex tc;

Metoda afişează ţine cont de semnul părţii imaginare. Dacă aceasta este negativă,
semnul minus este afişat înaintea simbolului i al părţii imaginare. Se declară
obiectul tc de tipul Complex.

Accesul la membrii obiectului se face folosind operatorul de calificare:

nume_obiect.nume_membru

unde numele obiectului specifică din ce obiect este accesat atributul respectiv sau
în contextul cărui obiect se execută metoda respectivă. Acest mod de accesare este
folosit atunci când se lucrează cu obiecte statice. În cazul în care nu avem un obiect
ci un pointer către un obiect, este necesară şi dereferenţierea pointerului, înainte de
accesul la membri. Acest lucru este realizat folosind operatorul -> în locul
operatorului de calificare:
p_obiect -> nume_membru

Pentru implementarea conceptului de încapsulare, în interiorul unei


definiţii de clasă pot fi folosiţi modificatori de acces. Aceştia sunt private,
protected şi public (urmaţi de caracterul: „două puncte”). Domeniul de acţiune al
unul modificator de acces începe în locul unde apare el şi se încheie la apariţia altui
modificator sau la terminarea descrierii clasei. Implicit, toţi membri sunt
consideraţi sub influenţa modificatorului private, deci orice membru aflat în afara
domeniului de acţiune al unui modificator este considerat privat. Modificatorul
public face ca toţi membri aflaţi în domeniul său de acţiune să poată fi accesaţi atât
de către metodele clasei, cât şi de către orice entitate din afara clasei (membri
publici). Modificatorul private face ca membrii aflaţi în domeniul său de acţiune să
poată fi accesaţi numai de către metodele clasei respective (membri privaţi).
Modificatorul protected este similar cu modificatorul private, dar membrii
respectivi pot fi accesaţi şi de către metodele claselor derivate, dar numai în obiecte
aparţinând claselor derivate.

195
Programarea calculatoarelor

De obicei atributele unei clase sunt declarate ca fiind private, iar metodele
sunt împărţite, unele fiind publice (interfaţa clasei) şi unele private (detalii şi
mecanisme interne de implementare a clasei. Deşi este tehnic posibil ca toţi
membrii unei clase să fie privaţi, un obiect de acest tip nu poate fi folosit, neavând
o interfaţă cu mediul exterior lui. De asemenea, toţi membrii unei clase pot fi
publici, dar nu este recomandată această tehnică din motive de protecţie şi
securitate.

Exemplu
În acest context, clasa Complex definită în exemplul anterior nu poate fi folosită,
toţi membrii ei fiind privaţi. Pentru a putea folosi obiecte de tipul Complex, metoda
afişează trebuie să fie publică. Descrierea clasei devine:
class Complex { float p_reala,p_imaginara;
public:
void Afiseaza();
};

void Complex::Afiseaza()
{ printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-',
p_imaginara>=0?p_imaginara:-p_imaginara);
}

Prin adăugarea modificatorului de acces public, metoda afişează este pusă la


dispoziţia mediului extern, ca interfaţă a obiectului.

Pentru accesul controlat la atributele private, clasele pot pune la dispoziţia


mediului extern metode publice de acces, numite uzual metode accesorii. Acestea
au rolul de a prezenta mediului extern valorile unora dintre atribute (acelea care pot
prezenta interes) sau de a modifica valorile unora dintre atribute, în mod controlat
(numai acele atribute pentru care modificarea la iniţiativa mediului extern are sens).
Controlul depinde în fiecare caz de scopul clasei respective.

Exemplu
Adăugând metode accesorii clasei Complex, descrierea acesteia devine:

class Complex { float p_reala,p_imaginara;


public:
void Afiseaza();
float GetR();
float GetI();
void SetR(float r);
void SetI(float i);
};

void Complex::Afiseaza()
{ printf("\n%5.2f%ci*%5.2f\n",p_reala,p_imaginara>=0?'+':'-',
p_imaginara>=0?p_imaginara:-p_imaginara);
}

196
Elemente de programare orientată obiect

float Complex::GetR()
{ return p_reala;
}
float Complex::GetI()
{ return p_imaginara;
}
void Complex::SetR(float r)
{ p_reala=r;
}
void Complex::SetI(float i)
{ p_imaginara=i;
}

Metodele accesorii definite mai sus (GetR, GetI, SetR, SetI) au rolul de a prezenta
valorile atributelor şi respectiv de a stabili noi valori pentru ele. În acest exemplu
nu se face nici un fel de control asupra modului în care sunt stabilite noile valori.
Folosind descrierile de mai sus, următoarea secvenţă de program:
void main()
{ Complex tc;
Complex *pc;
tc.SetR(5);
tc.SetI(-4);
tc.Afiseaza();
pc=&tc;
pc->Afiseaza();
pc->SetR(-2);
pc->SetI(3);
pc->Afiseaza();
}
produce pe ecran următorul rezultat:
5.00-i* 4.00

5.00-i* 4.00

-2.00+i* 3.00

Pentru a evita eventualele confuzii, în interiorul corpului metodelor poate


fi folosit pointerul implicit this atunci când se referă membrii obiectului curent.
Acesta este gestionat automat şi are ca valoare adresa obiectului curent. Ca urmare,
metoda SetR ar putea fi rescrisă astfel:
void Complex::SetR(float r)
{ this -> p_reala = r;
}

Pointerul this poate fi folosit şi pentru accesarea metodelor obiectului curent, în


acelaşi mod.
Ţinând cont de domeniul de valabilitate al declaraţiilor de tipuri de date,
descrierea unei clase se face de obicei la începutul fişierului sursă în care urmează a
fi folosită. Pentru o mai mare generalitate şi reutilizare mai uşoară, se preferă ca
fiecare clasă nouă să fie descrisă într-un fişier sursă separat, care să fie inclus
folosind directiva #include în programe.

197
Programarea calculatoarelor

10.3 Constructori
Declararea obiectelor are ca efect alocarea de spaţiu în memorie, la fel ca
în cazul declarării oricărei variabile. Acest spaţiu nu este iniţializat însă. Mai mult,
în cazul în care obiectele clasei au şi spaţiu extins de memorie, acesta nu este alocat
automat, obiectul declarat fiind astfel incomplet. Atributele unui obiect nu pot fi
iniţializate la declarare într-o manieră asemănătoare datelor de tip articol (struct),
deoarece de obicei atributele sunt private, deci inaccesibile din exteriorul
obiectului. Pentru rezolvarea problemei iniţializării obiectelor există posibilitatea
utilizării unor metode speciale, numite constructori. La terminarea ciclului de viaţă
al obiectelor, este necesară dezalocarea lor. În general, aceasta se realizează
automat, dar în cazul lucrului cu spaţiu extins, ea trebuie gestionată în mod explicit.
Problema încheierii ciclului de viaţă al obiectelor este rezolvată prin utilizarea unor
metode speciale numite destructori. Constructorii şi destructorii nu întorc niciun
rezultat prin numele lor şi antetele lor nu precizează nici un tip pentru rezultat (nici
măcar void).
Constructorii sunt metode care au acelaşi nume cu clasa căreia îi aparţin. O
clasă poate avea mai mulţi constructori, cu liste diferite de parametri (ca tip şi/sau
număr) – metode supraîncărcate. Dacă nu este definit niciun constructor pentru o
clasă, compilatorul va genera un constructor implicit, care nu face decât alocarea
spaţiului propriu al obiectului, în momentul în care acesta a fost declarat. Ca
urmare, în acest caz vom avea obiecte neiniţializate, urmând ca iniţializarea
atributelor să se facă ulterior, prin intermediul metodelor accesorii. În exemplul
anterior, pentru clasa Complex s-a generat un constructor implicit care alocă spaţiu
pentru atributele p_reala şi p_imaginara. Iniţializarea s-a făcut prin intermediul
metodelor SetR şi SetI. În cazul în care clasa prezintă cel puţin un constructor
explicit, compilatorul nu mai generează constructorul implicit. Ca urmare nu se vor
putea declara obiecte neiniţializate dacă parametrii constructorului nu au valori
implicite.
Constructorii nu pot fi apelaţi explicit, precum metodele obişnuite. Apelul
lor se realizează numai la declararea obiectelor. De asemenea, nu se poate
determina adresa constructorilor, aşa cum se poate face în cazul funcţiilor
obişnuite. Am văzut mai sus că declaraţia unui obiect care nu are constructor
explicit este identică cu declaraţia unei variabile simple. În cazul în care clasa
prezintă constructori expliciţi valorile pentru iniţializare sunt transmise acestuia la
declararea obiectului, asemănător listei de parametri reali la apelul unei funcţii:
nume_clasă nume_obiect(lista_valori);

198
Elemente de programare orientată obiect

Exemplu
Pentru clasa Complex se poate defini un constructor care să iniţializeze cei doi
membri astfel:
class Complex { float p_reala,p_imaginara;
public:
Complex(float a,float b);
};
Complex::Complex(float a,float b)
{ p_reala=a;
p_imaginara=b;
}
Având acest constructor în cadrul clasei, nu putem declara obiecte neiniţializate (ca
în exemplele anterioare), ci doar obiecte iniţializate:
Complex a(3,4); //a reprezintă numarul 3+i*4
Complex b(-1,3.2); //b reprezintă numarul -1+i*3.2
Complex c; //incorect

Atunci când există un singur constructor explicit, se aplică regulile


transferului parametrilor similar ca la funcţiile obişnuite (se realizează conversia
parametrilor reali către tipurile formale). Dacă sunt mai mulţi constructori, cu liste
diferite de parametri, se alege acela a cărui listă de parametri corespunde ca tip şi
număr cu lista valorilor (expresiilor) precizate la declararea obiectului.
Pentru constructori, ca şi pentru orice altă funcţie, pot fi precizate valori
implicite ale parametrilor. Acolo unde la apel lipsesc parametrii actuali, se folosesc
valorile implicite. Ca urmare, la declararea obiectelor pot să nu fie precizate valori
pentru toate atributele.

Exemplu
Se defineşte clasa Complex astfel:
class Complex { float p_reala,p_imaginara;
public:
Complex(float a=0,float b=0);
};
Complex::Complex(float a,float b)
{ p_reala=a;
p_imaginara=b;
}
Putem declara următoarele obiecte:
Complex a; //a reprezintă numarul 0+i*0
Complex b(-1); //b reprezintă numarul -1+i*0
Complex c(2,3); //c reprezintă numarul 2+i*3

Dacă prototipul constructorului ar fi fost Complex(float a,float b=0); atunci


la declaraţia obiectului trebuie precizată obligatoriu valoarea pentru primul
parametru (cel care va deveni valoarea atributului p_reala); ca urmare, dintre
declaraţiile anterioare ar fi fost corecte numai cele ale obiectelor b şi c.

199
Programarea calculatoarelor

Acolo unde se poate folosi un singur parametru la declararea obiectului, se


poate face iniţializarea asemănător iniţializării variabilelor simple. Folosind oricare
dintre cei doi constructori din exemplul anterior, putem declara un obiect şi în
modul următor:
Complex a = 1; //numarul 1+i*0
Valoarea declarată este atribuită primului parametru al constructorului.
Pentru situaţiile în care avem nevoie atât de obiecte iniţializate, cât şi de
obiecte neiniţializate, se adaugă în descrierea clasei atât un constructor care
realizează iniţializarea obiectului, cât şi un constructor vid, care simulează
constructorul implicit, adăugat de compilator atunci când nu se definesc
constructori impliciţi. Constructorul vid are următoarea formă:

Complex::Complex()
{
}

În acest context, declaraţia Complex a; are ca efect crearea obiectului a


neiniţializat (se utilizează constructorul vid, nu constructorul cu valori implicite
pentru parametri).
Parametrii unui constructor pot fi de orice tip, mai puţin de tipul clasei
respective (în exemplele anterioare, constructorul clasei Complex nu poate avea un
parametru de tipul Complex. Este posibil însă să avem un parametru de tip pointer
către clasa respectivă sau referinţă2 către clasa respectivă. Un constructor care
primeşte ca parametru o referinţă către un obiect din acea clasă se numeşte
constructor de copiere. Prin utilizarea unui constructor de copiere se poate
iniţializa un obiect nou cu atributele unui obiect existent (se realizează o copie a
acelui obiect). Constructorii de copiere pot avea şi alţi parametri, dar aceştia trebuie
să aibă valori implicite. Compilatorul generează automat un constructor de copiere
pentru toate clasele care nu au un constructor de copiere definit explicit.

Exemplu
Clasa Complex conţine un constructor de copiere:

class Complex { float p_reala,p_imaginara;


public:
Complex(float a=0,float b=0);
Complex(Complex &x);
};

2
În C++ este implementat transferul parametrilor prin adresă. Pentru a transmite un parametru prin
adresă, în lista de parametri se pune înaintea numelui său operatorul de referenţiere &

200
Elemente de programare orientată obiect

Complex::Complex(float a,float b)
{ p_reala=a;
p_imaginara=b;
}
Complex::Complex(Complex &x)
{ p_reala=x.p_reala;
p_imaginara=x.p_imaginara;
}

Putem declara următoarele obiecte:


Complex a(3,4); //numarul 3+i*4
Complex b = a; //numarul 3+i*4
Complex c(b); //numarul 3+i*4
Obiectul b este o copie a obiectului a şi va avea aceeaşi stare, imediat după
declarare (aceleaşi valori ale atributelor). De asemenea, obiectul c este o copie a
obiectului b. Pentru ambele obiecte s-a apelat constructorul de copiere.
Constructorul implicit de copiere realizează o copie binară a spaţiului
propriu de memorie al obiectului sursă. Ca urmare, dacă obiectele au spaţiu extins
de memorie, în urma copierii ambele obiecte referă acelaşi spaţiu extins de
memorie. Pentru a evita această situaţie anormală, atunci când clasele descriu
obiecte care lucrează şi cu spaţiu extins este necesară folosirea unui constructor de
copiere explicit, care să realizeze, pe lângă copierea atributelor, alocarea de spaţiu
extins propriu pentru obiectul nou şi copierea spaţiului extins al obiectului sursă în
acest spaţiu nou.
Constructorii nu pot fi apelaţi în mod explicit în program. Singura situaţie
în care poate să apară un astfel de apel este la declararea unui obiect, în modul
următor:
Complex a = Complex(2,5);

În cazul în care o clasă are ca membri obiecte, la declararea unui obiect al


clasei se apelează întâi constructorii pentru obiectele membru şi apoi constructorul
noului obiect.

10.4 Destructori
Destructorii sunt metode speciale, asemănătoare constructorilor, care au rol
invers: încheierea ciclului de viaţă al obiectelor. Aşa cum pentru fiecare clasă se
generează un constructor implicit (dacă nu a fost prevăzut unul explicit),
compilatorul generează şi un destructor implicit, dacă nu a fost prevăzut unul
explicit. Spre deosebire de constructori, o clasă poate avea numai un destructor
explicit. Ca şi constructorii, destructorii nu întorc niciun rezultat. Numele
destructorului este numele clasei precedat de caracterul ~ (tilda). Destructorii nu au
parametri.

201
Programarea calculatoarelor

Exemplu
class Complex { float p_reala, p_imaginara;
public
~Complex();
}
Complex::~Complex()
{ //descrierea corpului destructorului
}
Pentru clasa Complex destructorul nu are nimic de făcut şi nu e necesară descrierea
unui destructor explicit. Acest exemplu urmăreşte doar să arate cum se declară un
destructor explicit.
Spre deosebire de constructori, destructorii pot fi apelaţi explicit, atunci
când este necesară ştergerea unui obiect. Apelul se face la fel ca pentru orice altă
metodă, în contextul obiectului care trebuie şters:
a.~Complex();
Utilizarea destructorilor este obligatorie atunci când se lucrează cu date
dinamice, deoarece destructorii impliciţi nu pot elibera spaţiul alocat dinamic.
În cazul în care la crearea unui obiect au fost apelaţi mai mulţi constructori,
la ştergerea lui se apelează destructorii corespunzători, în ordine inversă.

10.5 Funcţii prieten


În unele situaţii este nevoie ca funcţii care nu sunt membri ai unei clase să
poată accesa atributele protejate ale clasei. În acest scop a fost introdus conceptul
de funcţie prieten în C++. O funcţie prieten nu face parte din clasă, dar poate
accesa atributele protejate.
Pentru a specifica o funcţie prieten, în interiorul descrierii clasei se scrie
prototipul funcţiei prieten, prefixat cu cuvântul rezervat friend. Întrucât funcţia
prieten nu este membru al clasei, în interiorul său nu este definit pointerul this, ceea
ce face ca funcţia prieten să nu poată accesa direct atributele obiectului. De aceea
este necesar ca obiectul să fie parametru al funcţiei prieten. Ca urmare, o funcţie
prieten are un parametru în plus faţă de o metodă.
Modificatorii de acces nu au nicio influenţă asupra funcţiilor prieten, de
aceea ele pot fi specificate oriunde în cadrul descrierii clasei.

Exemplu
În acest exemplu funcţia Afişează va fi scoasă în afara clasei Complex şi va fi
declarată ca funcţie prieten.
class Complex { float p_reala,p_imaginara;
public:
friend void Afiseaza(Complex x);
Complex(float a=0,float b=0);
Complex(Complex &x);
};

202
Elemente de programare orientată obiect

void Afiseaza(Complex x)
{ printf("\n%5.2f%ci*%5.2f\n",x.p_reala,x.p_imaginara>=0?'+':'-',
x.p_imaginara>=0?x.p_imaginara:-x.p_imaginara);
}
void main()
{ Complex a(1,2);
Afiseaza(a);
}
Exemplu
Să se implementeze clasa Stivă dinamică. O listă dinamică este formată din noduri,
deci putem defini întâi clasa Nod, urmând a folosi tipul Nod pentru a descrie clasa
Stivă. Pentru acest exemplu, datele memorate în nodurile stivei sunt de tip float.
#include <stdio.h>
typedef float TIP_INFO;
class Nod { TIP_INFO info;
Nod* next;
public:
float GetInfo();
Nod* GetNext();
Nod(float a, Nod* n);
};
float Nod::GetInfo()
{ return info;
}
Nod* Nod::GetNext()
{ return next;
}
Nod::Nod(float a, Nod* n)
{ info=a;
next=n;
}
class Stiva { Nod* Cap;
public:
Stiva();
Stiva(float a);
~Stiva();
void Push(float a);
float Pop();
int Empty();
void Afiseaza();
};
void Stiva::Afiseaza()
{ Nod* x;
x=Cap;
while(x)
{ printf("%5.2f ",x->GetInfo());
x=x->GetNext();
}
}
Stiva::~Stiva()
{ Nod* x;
while(Cap)
{ x=Cap;
Cap=Cap->GetNext();
delete x;
}
}

203
Programarea calculatoarelor

float Stiva::Pop()
{ float x;
Nod* y;
y=Cap;
x=Cap->GetInfo();
Cap=Cap->GetNext();
delete y;
return x;}
void Stiva::Push(float a)
{ Cap=new Nod(a,Cap);
}
int Stiva::Empty()
{ return Cap?0:1;
}
Stiva::Stiva(float a)
{ Cap= new Nod(a,NULL);
}
Stiva::Stiva()
{ Cap=NULL;
}
void main()
{ Stiva s;
int i;
float x;
if(s.Empty()) printf("\nStiva este goala");
else printf("\nStiva contine date");
for(i=0;i<10;i++)
s.Push((float)i);
s.Afiseaza();
x=s.Pop();
s.Afiseaza();
if(s.Empty()) printf("\nStiva este goala");
else printf("\nStiva contine date");
}

Clasa Nod conţine atributele info (informaţia utilă din nod) şi next, iar ca metode
un constructor care iniţializează atributele obiectului şi metode accesorii pentru
accesarea valorilor atributelor.
Clasa Stiva conţine un singur atribut, Cap care are ca valoare adresa primului nod
al stivei (vârful stivei). Constructorii clasei asigură crearea unei stive vide sau a
unei stive cu un element. Metodele asigură adăugarea unei informaţii în stivă,
respectiv extragerea unei informaţii. Metoda Afiseaza asigură afişarea pe ecran a
informaţiilor din stivă.

10.6 Derivarea claselor


Derivarea claselor este legată de implementarea conceptului de moştenire.
În limbajul C++ este permisă moştenirea multiplă. Pentru a defini o clasă fiu ca
fiind derivată dintr-o clasă părinte (sau mai multe clase părinte), se procedează
astfel:
class nume_clasa_fiu : lista_clase_părinte
{ descriere membri noi ai clasei fiu};

204
Elemente de programare orientată obiect

În lista claselor părinte se specifică numele claselor părinte, separate prin


virgulă şi, eventual, precedate de modificatori de acces – se pot folosi modificatorii
public sau private. Aceşti modificatori de acces definesc nivelul de protecţie a
membrilor clasei părinte în clasa fiu, conform tabelului următor:

Nivel acces Modificator de acces Nivel acces


în clasa părinte în lista claselor părinte în clasa fiu
public inaccesibil
private
private inaccesibil
public protected
protected
private private
public public
public
private private

Pentru fiecare clasă părinte se poate specifica un modificator de acces.


Dacă nu se specifică niciun modificator de acces atunci, implicit, se consideră
modificatorul public.
Se observă că o clasă derivată are acces la membrii clasei părinte care au
fost definiţi ca fiind publici sau protejaţi şi nu are acces la membrii privaţi. Prin
derivare se construiesc ierarhii de clase, deci din clasa fiu se pot deriva alte clase
noi. Dacă la derivare s-a folosit modificatorul de acces private, atunci toţi membrii
clasei părinte vor deveni privaţi în clasa fiu şi o derivare în continuare nu mai este
posibilă, ei fiind inaccesibili pentru orice altă clasă derivată din clasa fiu. Întrucât
ierarhiile de clase nu sunt definitive, ci oferă posibilitatea extinderii prin adăugarea
de noi clase derivate, se preferă ca la derivare să se folosească modificatorul de
acces public. De aceea acesta este modificatorul explicit.

Exemplu
Fie clasa Punct care implementează entitatea punct geometric. Aceasta are
atributele x şi y, care reprezintă coordonatele punctului în plan. Este inclusă o
singură metodă, care desenează punctul pe ecran (această metodă nu va
implementată în acest exemplu).
class punct { int x,y;
public:
void deseneaza();
};

void punct::deseneaza()
{ //corpul nu este descris in acest exemplu
}
Fie clasa Cerc care implementează entitatea geometrică cerc. Aceasta este descrisă
prin coordonatele centrului cercului şi raza sa. Ca urmare clasa Cerc poate fi
derivată din clasa Punct, adăugând un nou atribut (raza) şi o nouă metodă, pentru
desenarea cercului.

205
Programarea calculatoarelor

class cerc: punct


{ float raza;
public:
void deseneaza();
};
void cerc::deseneaza()
{ //corpul nu este descris in acest exemplu
}

Clasa cerc o să aibă ca membri atributele x, y şi raza şi metodele desenează


(moştenită de la clasa Punct, care va fi folosită pentru desenarea centrului cercului)
şi desenează (nou definită, care va fi folosită pentru desenarea cercului).

În lucrul cu ierarhii de clase se pune problema compatibilităţii tipurilor de


date (clase) în cadrul atribuirilor şi a conversiilor tipurilor de date. Ca principiu, un
obiect al unei clase părinte poate primi ca valoare un obiect al unei clase derivate.
Acelaşi principiu este valabil şi în cazul pointerilor către obiecte. Utilizând
exemplul de mai sus şi declaraţiile

punct a, *pp;
cerc b, *pc;

sunt corecte atribuirile

pp=&a;
pc=&b;
a=b;
pp=pc;
pp=&b;
Nu sunt corecte următoarele atribuiri:
pc=&a;
b=a;
pc=pp;

Pot fi realizate atribuiri folosind conversia explicită a tipurilor de date, astfel:

pc=(cerc *)pp;
pc=(cerc *)&a;

10.6.1 Redefinirea atributelor

Este posibil ca o clasă fiu să redefinească atribute moştenite de la clasa


părinte (atribute publice sau protejate, întrucât cele private sunt oricum inaccesibile
în clasa fiu). În acest caz, clasa fiu va avea două atribute cu acelaşi nume. Implicit,
utilizarea numelui atributului respectiv referă atributul redefinit. Pentru a accesa
atributul moştenit, trebuie folosit operatorul de rezoluţie, prefixând numele
atributului cu numele clasei părinte.

206
Elemente de programare orientată obiect

Exemplu
Fie o clasă Clasa_parinte care are un atribut a de tip float, atribut protejat, şi o
clasă Clasa_fiu care redefineşte atributul a, de tip double. x este un obiect de tipul
Clasa_fiu.
class Clasa_parinte
{ protected:
float a;
//descrierea restului clasei
};
Class Clasa_fiu: public Clasa_parinte
{ protected:
double a;
//descrierea restului clasei
}
Clasa_fiu x;

Expresia
x.a
referă atributul a al clasei derivate, de tip double. Pentru a accesa atributul a
moştenit de la clasa părinte, în cadrul unei metode a obiectului x trebuie folosită
expresia
Clasa_parinte::a

10.6.2 Redefinirea metodelor

La fel ca în cazul atributelor, o clasă fiu poate să redefinească metodele


moştenite de la clasa părinte, în cazul în care metoda moştenită nu corespunde
necesităţilor. În exemplul anterior, clasa Cerc redefineşte metoda desenează. Dacă
a este un obiect de tipul Cerc, atunci apelul a.desenează(); sau
desenează(); – efectuat din interiorul clasei Cerc – va lansa în execuţie metoda
redefinită, cea descrisă de clasa Cerc, care va desena conturul cercului. Atunci când
e nevoie să se apeleze metoda moştenită numele acesteia se prefixează cu numele
clasei părinte, folosind operatorul de rezoluţie:
punct::deseneaza();

Un astfel de apel poate să apară în interiorul metodei desenează a clasei Cerc


pentru a desena centrul cercului.

10.6.3 Constructori şi destructori în relaţia de moştenire

Constructorii şi destructorii nu se moştenesc precum alte metode. La


crearea unui obiect al unei clase fiu se apelează întâi constructorul clasei părinte şi
apoi constructorul clasei fiu. Dacă sunt mai multe clase părinte (moştenire
multiplă) se apelează constructorii claselor părinte, în ordinea în care acestea apar
în lista claselor părinte. La ştergerea unui obiect al unei clase fiu se apelează
destructorii în ordine inversă faţă de constructori: întâi destructorul clasei fiu

207
Programarea calculatoarelor

şi apoi destructorii claselor părinte, în ordine inversă celei în care acestea apar în
lista claselor părinte.
Pentru a preciza parametrii reali utilizaţi pentru fiecare din constructorii
claselor părinte, antetul constructorului clasei derivate are o formă specială:
class Clasa_fiu: clasa_p1, clasa_p2, clasa_p3
{ //attribute
public:
Clasa_fiu(); //constructorul clasei fiu
}

Clasa_fiu::clasa_fiu(…):clasa_p1(…),clasa_p2(…),clasa_p3(…)
{ //descrierea corpului constructorului
}

Se observă că în descrierea clasei, constructorul se descrie în mod obişnuit,


dar ulterior, în antetul constructorului apar apeluri ale constructorilor claselor
părinte. Ordinea în care apar aceste apeluri nu are nici o importanţă, deoarece
ordinea de apelare este dată de ordinea în care sunt specificate clasele părinte.
Dacă una din clasele părinte nu are constructor, atunci nu o să apară un
apel corespunzător în antetul constructorului clasei fiu, pentru ea apelându-se
automat constructorul implicit.
Dacă niciuna dintre clase nu are constructor explicit, atunci se folosesc
constructorii impliciţi pentru toate clasele.
O situaţie deosebită este aceea în care clasa fiu nu are constructor explicit,
dar cel puţin una din clasele părinte are un constructor explicit. Deoarece în această
situaţie nu se pot descrie explicit parametrii pentru apelarea constructorilor claselor
părinte, aceşti constructori trebuie să aibă valori implicite pentru toţi parametrii.

10.6.4 Clase virtuale

În cazul moştenirii multiple pot să apară situaţii în care o clasă derivate


moşteneşte un atribut (sau mai multe) de mai multe ori, prin intermediul mai multor
linii de moştenire. Aceste situaţii produc ambiguităţi legate de referirea atributului
respectiv.
Limbajul C++ oferă un mecanism simplu prin care să se revină astfel de
situaţii, prin utilizarea claselor virtuale. Fie următoare ierarhie, în care clasa cf este
derivată din clasele cp1, cp2 şi cp3, toate acestea fiind la rândul lor derivate din
clasa cb. Clasa de la baza ierarhiei, cb are un atribut x. Acest atribut va fi moştenit
în clasele cp1, cp2, cp3. Clasa cf va moşteni 3 exemplare ale atributului x.

208
Elemente de programare orientată obiect

Figura 10.5 Exemplu de moştenire multiplă

Pentru a evita această situaţie, clasa cb poate fi declarată ca virtuală la


descrierea claselor cp1, cp2 şi cp3, astfel:
class cp1: virtual public cb
{
};
class cp2: virtual public cb
{
};
class cp3: virtual public cb
{
};
class cf: public cp1, public cp2, public cp3,
{
};

Considerând aceste declaraţii, clasa cf moşteneşte o singură dată atributul


x, prin intermediul clasei cp1. Ca principiu, atributul este moştenit prin intermediul
clasei care apare prima în lista claselor părinte.
În cazul în care în lista claselor părinte apar şi clase virtuale, se apelează
întâi constructorii claselor virtuale, în ordinea în care au fost specificate, apoi
constructorii claselor nevirtuale, în ordinea în care sunt acestea specificate.

10.6.5 Funcţii virtuale

Există situaţii în care nu se poate decide în momentul compilării care este


contextul curent în care se apelează o metodă, care a fost redefinită într-o clasă fiu.
Astfel de situaţii apar atunci când se lucrează cu pointeri. Fie o clasă cp care
conţine metoda executa, şi o clasă derivată din ea, numită cf, care redefineşte

209
Programarea calculatoarelor

metoda executa, cu aceeaşi listă de parametri. Fie următoarele declaraţii:

cp a; //a este obiect de tipul cp


cf b; //b este obiect de tipul cf
cp* po; // po este pointer catre clasa cp

Pointerul po poate lua ca valoare atât adresa unui obiect de tipul cp, cât şi adresa
unui obiect de tipul cf.
Fie apelul
po->executa();

În momentul compilării nu se poate stabili ce metodă să se apeleze, a clasei părinte


sau a clasei fiu. În astfel de situaţii compilatorul generează un apel către metoda
clasei părinte.
Limbajul C++ oferă posibilitatea de a întârzia decizia până la momentul
execuţiei. În acest scop metoda clasei părinte se declară ca fiind virtuală prin
scrierea cuvântului rezervat virtual înaintea antetului său. Este suficient ca metoda
clasei de bază să fie declarată ca fiind virtuală, în mod automat şi metodele claselor
derivate vor fi virtuale.
Constructorii, destructorii şi funcţiile inline nu pot fi virtuale.

10.6.6 Clase abstracte

În limbajul C++ este definit conceptul de funcţie virtuală pură. Acest


concept este necesar în cazul ierarhiilor de clase. Există situaţii în care toate clasele
ierarhiei trebuie să conţină o anumită metodă, implementarea ei fiind diferită în
fiecare clasă derivată. Clasa de la baza ierarhiei este prea generală pentru a putea
implementa metodele. În această situaţie în clasa de bază se includ metode virtuale
pure. Prezenţa unei astfel de metode obligă toate clasele derivate să o conţină, fie
că o redefinesc fie că nu.
O metodă virtuală pură se declară asemănător cu o metodă virtuală,
adăugând la sfârşitul antetului =0:

virtual tip_rezultat nume_metoda(lista parametri) =0;

Metodele virtuale pure nu au un corp, nefiind implementate.


O clasă care conţine o metodă virtuală pură se numeşte clasă abstractă. O
clasă abstractă nu poate fi instanţiată, ea conţinând metode care nu sunt
implementate.
Clasele derivate din clase abstracte pot să redefinească metodele virtuale
pure sau nu. Dacă metoda virtuală pură nu este implementată, atunci şi clasa
respectivă este clasă abstractă şi nu poate fi instanţiată.

210
Elemente de programare orientată obiect

De exemplu, într-o ierarhie de clase care descriu figuri geometrice, fiecare


clasă are nevoie de metode pentru desenarea figurii respective, calculul ariei sau
perimetrului. La baza ierarhiei se află o clasă abstractă care include metode virtuale
pure pentru aceste operaţii. Clasele derivate, vor implementa metodele conform
specificului fiecărei figuri geometrice.

211
Bibliografie

1. [Aho, Hop şa] Aho A., Hopcroft J., Ullman J., Data Structures and
Algorithms, Addison-Wesley, 1983
2. [Bras, Brat] Brassard G., Bratley P., Algoritmics: Theory and Practice,
Prentice-Hall, 1988
3. [Cor, Lei şa] Cormen T., Leiserson C., Rivest R., Introduction to
Algorithms, MIT Press, sixteenth printing, 1996
4. [Ghilic, 2003] Ghilic-Micu Bogdan, Roşca Ion Gh., Apostol Constantin,
Stoica Marian, Lucia Cocianu Cătălina, Algoritmi în
programare, Bucureşti, Editura ASE, 2003
5. [Gon] Gonnet G.H., Handbook of Algorithms and Date Structures,
Addison-Wesley, 1984
6. [Hor] Horowitz E., Sahni S., Fundamentals of Computer Algorithms,
Computer Science Press, 1978
7. [Knu] Knuth D., Fundamental Algorithms, vol 1 of The Art of Computer
Programming, Addison-Wesley, 1973
8. [Knu] Knuth D., Sorting and Searching, vol 3 of The Art of Computer
Programming, Addison-Wesley, 1973
9. [Negrescu, 1994] Negrescu Liviu, Limbajele C şi C++ pentru
începători, Cluj-Napoca, Editura Microinfomatica, 1994
10. [Man] Manmber U., Introduction to Algorithms: A Creative Approach,
Addison-Wesley, 1989
11. [Pop, Geo şa] Popovici Ct., Georgescu H., State L., Bazele informaticii,
vol 1, Tip. Universităţii din Bucureşti, 1990
12. [Smeureanu, 1995] Ion Smeureanu, Ion Ivan, Marian Dârdală, Limbajul
C/C++ prin exemple, Bucureşti,Editura Cison, 1995
13. [Tom] Tomescu I.., Probleme de combinatorică şi teoria grafurilor,
Bucureşti, Editura Didactică şi Pedagogică, 1981
14. [Tud] Tudor S., Tehnici de programare, Bucureşti, Editura Teora, 1994
15. [Wil] Wilf H., Algorithms and Complexity, Prentice-Hall, 1986

212

S-ar putea să vă placă și