Sunteți pe pagina 1din 192

UNIVERSITATEA SPIRU HARET

FACULTATEA DE INGINERIE, INFORMATICĂ ȘI GEOGRAFIE

CUNOSTINTE FUNDAMENTALE SI
DE SPECIALITATE
- Versiune DRAFT -

---- 2017 ----

*Material preliminar in vedere pregatirii examenului de licenta in Informatica

NOTA: Nicio parte a acestui material nu va fi utilizata in alt scop decat in vederea pregatirii Probei I a
examenului de licenta in INFORMATICA. Materialul este in curs de editare si finalizare in vederea
publicarii la Editura Fundatiei Romania de Maine.

1
Cuprins

I. Programare orientată pe obiecte ................................................................................6


1. Fundamente ale programării orientate pe obiecte .............................................................................6

1.1. Concepte POO: clasă, obiect (identificare unică, interfaţă, stare, mesaj), metodă ..................6

1.2. Principiile POO: încapsulare, moştenire, polimorfism ..........................................................7

2. Implementarea POO în C++ ........................................................................................................8

2.1. Definirea claselor ................................................................................................................8

2.2. Constructori, destructori, obiecte C++ ................................................................................ 10

2.3. Clase derivate: definire, accesul la membrii clasei, constructorii şi destructorii în contextul
claselor derivate, tipuri de constructori. ......................................................................................... 15

2.4. Metode virtuale şi clase abstracte. ...................................................................................... 17

2.5. Supraîncărcarea metodelor ................................................................................................. 19

2.6. Metode inline, funcţii friend .............................................................................................. 21

2.7. Supraîncărcarea operatorilor .............................................................................................. 23

2.7.1. Supraîncărcarea cu funcţii operator membre ............................................................... 24

2.7.2. Supraîncărcarea cu funcţii operator de tip friend ......................................................... 26

2.8. Fluxuri de intrare-ieşire în C++ .......................................................................................... 27

2.9. Tratarea excepţiilor ............................................................................................................ 29

2.10. Programare generică în C++. Biblioteca standard de şabloane. ....................................... 30

3. Antrenamente C++ ........................................................................................................................ 38

3.1. Noţiuni fundamentale ............................................................................................................. 38

3.2. Studii de caz ........................................................................................................................... 41

3.3. Probleme propuse spre rezolvare ............................................................................................ 50

Bibliografie....................................................................................................................................... 53

II. Baze de date ....................................................................................................................... 54


1. Noţiuni introductive în teoria bazelor de date ................................................................................ 54

2
1.1. Noțiunile de bază de date, sistem de gestiune a bazei de date ............................................. 54

1.2. Noțiunile de entitate, relație, atribut ................................................................................... 54

1.3. Construirea de diagrame entitate-relaţie ............................................................................. 56

1.4. Tipuri de relații între entităţi .............................................................................................. 57

2. Baze de date relaţionale................................................................................................................. 57

2.1. Noțiunile de bază de date relaţională, sistem de gestiune a bazelor de date relaţionale ............. 57

2.2. Regulile lui Codd ................................................................................................................... 58

2.3. Componentele bazelor de date relaționale: .............................................................................. 59

1) Structura relaţională a datelor ............................................................................................ 60

2) Operatorii modelului relaţional .......................................................................................... 60

3) Restricţii de integritate ale modelului relaţional ................................................................. 62

2.4. Tipuri de constrângeri de integritate........................................................................................ 63

3. Proiectarea bazelor de date relaţionale ........................................................................................... 63

3.1. Formele normale: FN1; FN2; FN3 .......................................................................................... 64

4. Limbajul SQL (Structured Query Language) ................................................................................. 70

4.1. Structura lexicală a limbajului SQL ................................................................................... 70

4.2. Operatori SQL ................................................................................................................... 70

4.3. Funcţii definite în SQL ...................................................................................................... 71

4.4. Tipuri de date .................................................................................................................... 72

4.5. Categorii de instrucţiuni SQL ............................................................................................ 73

5. Limbajul de definire a datelor (LDD) ............................................................................................ 73

5.1. Comenzi (CREATE, ALTER, DROP) .................................................................................... 74

6. Limbajul de manipulare a datelor (LMD) ...................................................................................... 78

6.1. Interogarea datelor (Comanda SELECT) ................................................................................ 79

6.2. Adăugarea de noi tupluri (Comanda INSERT) ........................................................................ 88

6.3. Modificarea tuplurilor din tabele (Comanda UPDATE) .......................................................... 91

6.4. Ştergerea tuplurilor din tabele (Comanda DELETE) ............................................................... 93

7. Limbajul de control al datelor (LCD) ............................................................................................ 94

7.1. Asigurarea confidențialității și securității datelor .................................................................... 95


3
7.2. Reluarea unor acțiuni în cazul unei defecțiuni ......................................................................... 95

7.3. Garantarea coerenței datelor în cazul prelucrării concurente .................................................... 96

8. Exerciţii de fixare a noţiunilor ....................................................................................................... 98

Bibliografie..................................................................................................................................... 108

III. Structuri de date ............................................................................................................ 110


1. Noțiuni teoretice – prezentare .................................................................................................. 110

1.1 Structuri de date statice: vectori, matrici .......................................................................... 110

1.2 Liste ................................................................................................................................ 111

1.2.1 Liste implementate secvențial .................................................................................. 112

1.2.2 Liste înlănțuite ......................................................................................................... 112

1.2.3 Liste dublu înlănțuite ............................................................................................... 115

1.3 Stive ................................................................................................................................ 116

1.3.1 Stiva secvențială ...................................................................................................... 116

1.3.2 Stiva simplu înlănțuită ............................................................................................. 117

1.4 Cozi ................................................................................................................................ 118

1.4.1 Coada secvențială .................................................................................................... 119

1.4.2 Coada simplu înlănțuită............................................................................................ 121

1.4.3 Coada dublu înlănțuită ............................................................................................. 123

1.5 Structuri arborescente – noțiuni fundamentale .................................................................. 123

1.6 Arbori binari, arbori binari de cǎutare .............................................................................. 126

1.7 Sortarea şi cǎutarea .......................................................................................................... 128

1.7.1 Sortarea prin numărare ............................................................................................. 129

1.7.2 Sortarea prin inserție ................................................................................................ 129

1.7.3 Sortarea prin metoda bulelor .................................................................................... 130

1.7.4 Sortarea prin interclasare.......................................................................................... 131

1.7.5 Sortarea rapidă ......................................................................................................... 134

1.7.6 Căutarea .................................................................................................................. 134

2. Înțelegerea conceptelor............................................................................................................ 135


4
3. Aplicarea și utilizarea noțiunilor teoretice ................................................................................ 139

Bibliografie..................................................................................................................................... 146

IV. Sisteme de operare ........................................................................................................148


1. Organizarea structurală a sistemelor de calcul. Fundamente ..................................................... 148

2. Structura sistemelor de operare.................................................................................................... 151

3. Gestiunea proceselor ................................................................................................................... 154

3.1. Fundamente.......................................................................................................................... 154

3.2. Sincronizarea proceselor ....................................................................................................... 157

3.2.1. Metoda variabilei poartă ................................................................................................ 158

3.2.2. Metoda alternării ........................................................................................................... 158

3.2.3. Metoda lui Peterson ....................................................................................................... 158

3.2.4. Metoda semafoarelor ..................................................................................................... 159

3.3. Comunicare între procese ..................................................................................................... 161

3.4. Planificarea proceselor ......................................................................................................... 162

4. Gestiunea memoriei interne. Memoria virtuală ............................................................................ 167

4.1. Generalităţi .......................................................................................................................... 167

4.2. Structuri de date şi algoritmi ................................................................................................ 168

4.3. Metode elementare de gestiune a memoriei operative ........................................................... 170

4.4. Metode avansate de gestiune a memoriei operative ............................................................... 171

4.4.1. Alocare continuă............................................................................................................ 171

4.4.2. Memoria virtuală ........................................................................................................... 172

5. Gestiunea memorie externe. Sisteme de fişiere. Tehnici input-output ........................................... 176

5.1. Gestiunea fişierelor .............................................................................................................. 176

5.2. Gestiunea cataloagelor.......................................................................................................... 179

5.3. Drivere de Intrare/Ieşire (I/E) ............................................................................................... 180

6. Protecţie şi securitate .................................................................................................................. 181

7. Interfeţe utilizator ....................................................................................................................... 182

8. Exerciţii şi probleme ................................................................................................................... 182

Bibliografie..................................................................................................................................... 192
5
I. Programare orientată pe obiecte
Grigore Albeanu, Prof.univ.dr., Universitatea Spiru Haret,
Alexandru Averian, Senior Software Developer at Luxoft,
Costinela Luminiţa Defta, Lector univ. dr., Universitatea Spiru Haret

1. Fundamente ale programării orientate pe obiecte

Programarea orientată pe obiecte (POO) presupune o bună experienţă în modelarea obiectuală. Întreg
procesul computaţional are la bază gândirea orientată pe obiecte. Concepte fundamentale, tehnici
elementare, dar şi modalităţi avansate de programare orientată pe obiecte vor fi prezentate în secţiunile
de mai jos.

1.1. Concepte POO: clasă, obiect (identificare unică, interfaţă, stare, mesaj), metodă

Clasele descriu caracteristicile de stare şi comportamentale ale obiectelor. Distingem două tipuri de
caracteristici: atribute (specifică trăsăturile obiectelor) şi metode (specifică comportamentul
obiectelor). Clasa este identificată printr-un nume unic în cadrul unui proiect. Acest nume identifică un
tip de date şi este utilizat pentru operaţii de declarare a tipului. Deci, o colecţie de obiecte caracterizate
similar din punct de vedere informaţional şi comportamental trebuie să aparţină unei clase. Clasa este
o noţiune abstractă care defineşte un anumit tip de obiecte. O clasă reprezintă mulţimea obiectelor de
acelaşi tip.

Crearea unui obiect presupune indicarea clasei din care face parte, astfel identificându-se proprietăţile
obiectului şi modul în care acestea obiectul se va comporta. Pentru un programator, un obiect contează
prin: identitate (trebuie să fie unică), interfaţă (partajată în comun cu alte obiecte de acelaşi tip prin
raportare la o clasă definitoare) şi stare (valorile curente ale atributelor).

Metodele unei clase realizează prelucrări asupra atributelor obiectului de destinaţie. Deoarece acesta
este unic, referirea caracteristicilor (atribute sau metode ) se face direct, utilizând numele acestora. O
metodă poate fi asemuită cu o funcţie de prelucrare ce are fixat unul din parametri (obiectul curent).

Pentru a evita crearea unor obiecte neiniţializate (cu o parte din atribute nedefinite), o clasă trebuie să
dispună de una sau mai multe metode speciale, de tip constructor, prin care se asigură iniţializarea
tuturor atributelor la momentul creării unui obiect. Numele constructorilor este identic cu numele
clasei. Când există mai mulţi constructori pentru aceeaşi clasă, ei sunt distinşi prin numărul diferit de
argumente sau prin tipul diferit al argumentelor. Pentru a fi utilizabili, constructorii trebuie să facă
parte din interfaţa clasei.

Clasele sunt de regulă asociate prin relaţii client-server. Orice clasă este construită pentru a oferi
anumite servicii unor obiecte din alte clase. Clasele care oferă servicii se numesc clase server. Clasele
ale căror obiecte utilizează serviciile se numesc clase client. Foarte frecvent o clasă joacă ambele
roluri: oferă servicii altor clase (are rol de server), dar pentru aceasta utilizează serviciile altor clase
(are rol şi de client). Clasa server oferă drept servicii o anumită parte a caracteristicilor sale (atribute
sau metode). Aceste caracteristici sunt declarate printr-un cuvânt cheie (de obicei “public”) şi
alcătuiesc o listă de servicii numită interfaţa clasei. Clasa client este interesată de lista de servicii
(interfaţa) clasei server, nu şi de algoritmii utilizaţi în implementare acestor servicii. Acele
6
caracteristici ale clasei server care sunt considerate critice pentru funcţionarea corectă ar trebui să nu
poată fi accesate direct de clasele client. Ele sunt declarate prin cuvântul cheie “private”. Este
recomandat ca atributele (de stare) să nu facă parte din interfaţa clasei.

Există un tip special de clase client ale unei clase server, numite subclase ale serverului. Acestea sunt
derivate direct din clasa server şi pentru implementarea lor este necesar accesul la anumite
caracteristici ale casei server. Pentru acest tip de acces, caracteristicile se declară prin cuvântul cheie
“protected”; o caractetristică protected nu este accesibilă altor clase client ci numai celor derivate
(extinse).

Sintetic, o clasă poate fi reprezentată precum în Fig. 1, în care caracteristicile din interfaţă (specificate
public) sunt marcate cu semnul +, cele specificate prin protected cu semnul #, iar cele specificate prin
private cu -.

Figura 1. Diagrama unei clase

Clasa client utlizează serviciile clasei server trimiţând mesaje către obiecte ale clasei server. Un mesaj
este un nume de caracteristică (în cele mai multe cazuri, o metodă) din interfaţa clasei server. Dacă de
exemplu serverul are numele S, obiectul are numele ob, iar mesajul este o metodă cu numele mes, fără
parametri, expresia prin care se transmite mesajul este ob.mes(). Obiectul ob se numeşte destinatar al
mesajului sau obiect curent al metodei mes. Ca urmare a transmiterii mesajului, obiectul ob răspunde
prin executarea implementării S::mes(), care prelucrează atributele obiectului curent ob. Efectul este
similar cu evaluarea expresiei mes(ob) din programarea procedurală.

1.2. Principiile POO: încapsulare, moştenire, polimorfism

Principiul încapsulării presupune două niveluri de abordare: 1) ca metodă de concepţie, încapsularea


se referă la capacitatea de a separa aspectele externe ale unui obiect (interfaţa), accesibile altor obiecte,
de aspectele interne ale obiectului (atribute interne, structuri de date, algoritmi), care sunt ascunse faţă
de celelalte obiecte; 2) ca implementare, la nivelul unui limbaj de programare, încapsularea este
asigurată de exigenţele sintactice specifice.

Posibilitatea de a reutiliza, prin adaptare, programele deja scrise, fără a interveni asupra textului sursă
deja existent, este unul din elementele cheie care justifică eficienţa tehnicilor de programare orientată
pe obiecte. Operaţiile de tip cut-copy-paste-insert asupra textului sursă al funcţiilor definite într-un
program, se transformă, în viziunea POO, în operaţii de specializare a claselor deja definite. Prin
crearea unor ierarhii de clase, în care unele provin din altele prin preluarea şi specializarea unor
caracteristici, POO oferă posibilitatea de adaptare a unor programe existente, în condiţiile în care se
dispune de textul sursă al definiţiilor claselor (fişierele antet) şi codul obiect al implementărilor.

Prin aplicarea principiului moştenirii se creează condiţii pentru specializarea unei clase existente.
Există mecanisme simple prin care se poate defini o clasă nouă S ce preia (moşteneşte) caracteristicile
unei clase deja existente C (numită şi superclasă sau clasă de bază), specializează unele din aceste
caracteristici pentru a le adapta unor situaţii noi şi adaugă alte caracteristici. Noua clasă se numeşte, în
7
acest caz, subclasă a clasei care a fost utilizată ca punct de plecare. Se spune în acest caz că S este
obţinută prin derivare din clasa C sau, şi mai sugestiv, prin specializarea clasei C. Relaţia de
specializare(derivare) se reprezintă prin diagrama din fig. 2 şi este un caz particular de relaţie client-
server (aici C este server, iar S client).

Figura 2. Relaţia de specializare

Trebuie reţinut că, în literatura de specialitate şi printre programatori, sunt utilizaţi cel puţin şapte
termeni echivalenţi în cadrul relaţiei de specializare: 1) C clasă, S subclasă; 2) C superclasă, S clasă;
3) C clasă de bază, S clasă derivată; 4) C clasă , S clasă specializată; 5) C clasă generalizată, S clasă;
6) C tip de date, S subtip de date; 7) C supertip de date, S tip de date.

Moştenirea poate fi simplă (clasa derivată preia atributele şi metodele unei singure clase de bază), sau
multiplă (clasa derivată preia atributele şi metodele mai multor clase de bază). Implementarea
moştenirii este realizată diferit în funcţie de limbajul de programare utilizat. De exemplu, în limbajul
Java este permisă moştenirea simplă, iar moştenirea multiplă este realizată indirect dintr-o clasă de
bază şi una sau mai multe interfeţe (aici cuvântul interfaă are alt sens decât cel prezentat mai sus).

Prin moştenire multiplă şi indirectă se crează ierarhii de clase, care sunt grafuri orientate aciclice (în
cazul moştenirii simple avem un arbore orientat).

Specializarea poate fi realizată prin adăugarea de noi operaţii celor moştenite sau prin redefinirea
unora dintre operaţiile moştenite. Specializarea orientată pe redefinirea operaţiilor se află la baza
implementării principiului polimorfismului. Aplicarea principiului polimorfismului creează
posibilitatea ca un mesaj (în structura căruia intervine un nume de operaţie) să genereze răspunsuri
diferite, în funcţie de contextul în care este formulat mesajul.

În POO se face diferenţă între supraîncărcare (overloading) şi redefinire (overriding). Supraîncărcarea


se referă la existenţa mai multor metode sau constructori cu acelaşi nume, dar cu signatură diferită.
Redefinirea se referă la definirea unei metode cu acelaşi nume, dar intr-o clasă derivată. Constructorii
nu pot fi redefiniţi, ei fiind legaţi de clasa în care sunt definiţi.

2. Implementarea POO în C++


În cele ce urmează vom descoperi modul în care sunt implementate principiile POO în limbajul C++.

2.1. Definirea claselor

O clasă C++ încapsulează date şi funcţii (membrii clasei) care beneficiază de anumite niveluri de
protecţie împotriva accesului extern. Metodele pot fi definite atât în interiorul clasei (inline) cât şi în
afara acestora, folosind operatorul de rezoluţie „::”. Forma generală a unei specificaţii de clasă este
următoarea:

class <Nume_clasă> [:<Lista_claselor_de_bază>] {


<Date şi funcţii particulare>
8
<Specificator de acces>:
<Date şi funcţii>
<Specificator de acces>:
<Date şi funcţii>
...........
<Specificator de acces>:
<Date şi funcţii>
} [<Lista de obiecte>];

unde:
 <Lista de obiecte> este opţională. Dacă există, ea declară obiecte din clasa definită.
 <Specificator de acces> este unul din cuvintele cheie: public, private şi protected.
 <Lista_claselor_de_bază> indică, opţional, clasele de la care se pot moşteni atribute
informaţionale şi comportamentale.
 <Nume_clasă> este identificatorul C++ care denumeşte în mod unic clasa.
 <Date şi funcţii particulare>: Implicit, datele şi funcţiile declarate într-o clasă sunt proprii
acelei clase, doar membrii săi având acces la ele.
 Odată utilizat un specificator, efectul său este valabil până la întâlnirea altui specificator de
acces sau a sfârşitului declaraţiei clasei.
 Folosind specificatorul de acces public, permitem funcţiilor sau datelor membre să fie
accesibile altor secţiuni ale programului care utilizează obiecte ale clasei. Revenirea la modul
privat de declarare a membrilor se realizează folosind specificatorul de acces private.
Specificatorul protected are implicaţii asupra vizibilităţii membrilor unei clase în cazul în
care se are în vedere moştenirea.
 Din punct de vedere practic se recomandă protecţia totală a datelor (folosind private) şi
definirea unor metode de tip GET (pentru citirea stării obiectului), respectiv SET (pentru
modificarea stării obiectului) care vor fi specificate prin public.
 Modificatorii de acces pot fi utilizaţi atât în cadrul claselor cât şi în cadrul structurilor.
Accesul este implicit public pentru structuri (struct) şi private pentru clase.
 O clasă poate oferi acces nerestricţionat la membrii proprii prin utilizarea cuvântului cheie
friend în una din formele:
friend prototip_funcţie;
sau
friend nume_clasă;
 În limbajul C++, structurile şi uniunile reprezintă cazuri particulare ale claselor, putând avea
nu numai date membre, câmpuri de date, dar şi funcţii membre. Singura diferenţă între
structuri şi uniuni constă în faptul că la uniuni, pentru memorarea valorilor datelor membre se
foloseşte aceeaşi zonă de memorie. Deosebirea esenţială dintre structuri şi uniuni - pe de o
parte - şi clase - pe cealaltă parte – constă în modul de acces la membrii: la structuri şi uniuni
membrii (datele si metodele) sunt implicit publici, iar la clase - implicit privaţi (fiind
încapsulaţi). Deci, lipsa unor modalităţi de protecţie a datelor, face ca tipurile de date
introduse prin struct sau union să nu poată fi strict controlate în ceea ce priveşte operaţiile
executate asupra lor.
 Datele membru se alocă distinct pentru fiecare instanţă a clasei. Excepţia de la această regulă
o constituie datele membru statice (introduse folosind cuvântul cheie static), care există într-
un singur exemplar, comun, pentru toate instanţele clasei.

9
2.2. Constructori, destructori, obiecte C++

Constructorul este o funcţie membru a clasei care are acelaşi nume cu clasa. Pentru fiecare obiect
declarat este apelat un constructor. Constructorul poate avea parametri formali, cu sau făra îniţializare.
Pentru a putea fi utilizat, constructorul trebuie să fie declarat public.

Un constructor al unui obiect este apelat o singură dată pentru obiecte globale sau pentru cele locale de
tip static. Constructorul este o funcţie fără tip ceea ce nu impune utilizarea cuvântului cheie void în
locul tipului. Pentru obiecte locale constructorul este apelat de fiecare dată când este întâlnită
declararea acestora.

Dacă programatorul nu specifică un constructor explicit la definirea unei clase, la crearea unei instanţe
a clasei (declararea unui obiect) se foloseşte constructorul implicit ataşat de compilator fiecărei clase.
Constructorul implicit nu are parametri formali, ceea ce are drept consecinţă faptul că nu este permisă
iniţializarea la declarare a datelor membre ale obiectelor. Constructorul implicit nu este generat în
cazul în care clasa are ataşat un alt constructor fără parametri.

Spre deosebire de funcţiile obişnuite, constructorii pot avea o listă de iniţializare (a se vedea Exemplul
2) pentru atributele clasei, de forma:

constructor(param constructor) : atribut_1(param), ...,


atribut_n(param) {...}

În C++ un obiect poate prelua starea unui obiect existent prin intermediul constructorului de copiere.
Deci, un constructor de copiere are rolul de a atribui datele unui obiect altuia. Altfel spus,
constructorul de copiere este un constructor care creează un obiect iniţializându-l cu un obiect din
aceeaşi clasă, care a fost creat anterior Preluarea datelor obiectului care se copiază utilizează
specificarea prin referinţă:

<Clasa> <Nume clasa> :: <Nume constructor> (<Nume clasa> & Obiect);

Constructorii de copiere se apelează numai la iniţializare, nu şi la atribuire. Mai precis, constructorul


de copiere este utilizat pentru: 1) iniţializarea unui obiect din altul de acelaşi tip; 2) copierea unui
obiect pentru a-l transmite unei funcţii; 3) copierea unui obiect pentru a-l returna dintr-o funcţie.

Dacă un constructor de copiere nu este definit într-o clasă, compilatorul însăşi defineşte unul. Dacă
clasa are variabile pointer şi are unele alocări dinamice de memorie, atunci este necesar să existe un
constructor de copiere.

Exemplul 1. Clasa Persoana din diagrama prezentată în fig. 3, are codul C++:

10
class Persoana{
// interfata
public:
// are doi constructori si metoda de
afisare
Persoana(char *nume, char *prenume, int
an_nastere=2000); //ultimul parametru este
implicit
Persoana();
void afisare();
void set_an_nastere(int an_nastere);
//sfarsit interfata
protected:
// parte inaccesibila clientilor, cu
exceptia subclaselor Figura 3. Diagrama clasei Persoana
// are trei atribute
char *n, *p; // nume, prenume
private:
int an; // an nastere
};

Complementul constructorului este destructorul. De multe ori un obiect trebuie să efectueze anumite
acţiuni când este distrus (de exemplu, trebuie să elibereze eventualele zone de memorie alocate de
către constructor). Este evident faptul că obiectele locale sunt distruse la părăsirea blocului în care
apar, iar obiectele globale la terminarea programului. Când este distrus un obiect, atunci este apelat
destructorul clasei definitoare.

Destructorul are acelaşi nume cu constructorul, dar precedat de un caracter ~. O clasă are un singur
destructor şi acesta nu poate avea parametri formali. Atât constructorul cât şi destructorul, în C++, nu
pot să returneze valori.

În C++, obiectele pot fi create static sau dinamic. Sintaxa pentru varianta statică este:
<Clasa> <Obiect>[(<Lista de valori>)];
sau
<Clasa> <Obiect>[=<Valoare>];
Sintaxa ne arată că odată cu crearea instanţei se poate face şi iniţializarea datelor membre ale
obiectului cu ajutorul constructorilor parametrizaţi.

În limbajul C, alocarea dinamică a memoriei este realizată cu ajutorul funcţiior malloc() şi free(). Din
motive de compatibilitate şi nu numai, aceste funcţii sunt valabile şi în C++. Totodată, C++ are un
sistem alternativ de alocare dinamică bazat pe operatorii new şi delete. Sintaxa generală pentru new şi
delete este:
<Pointer>=new <Tip>;
delete <Pointer> ;
unde <Pointer> este o variabilă pointer, compatibilă ca tip cu <Tip>. Aşadar, <Pointer> poate păstra
adresa către zona de memorie în care încap date având tipul <Tip>. Operatorul delete trebuie folosit
doar cu un pointer valid, alocat deja prin utilizarea operatorului new. În caz contrar, rezultatele sunt
imprevizibile.

11
În plus, operatorul new permite iniţializarea memoriei alocate unui tip de bază cu o valoare dată,
utilizând sintaxa:
<Pointer>=new <Tip> (<Valoare_iniţială>);
// ca în exemplul:int *p; p = new int(10);.
Matricile nu pot fi iniţializate în timpul alocării dinamice ce poate fi realizată folosind sintaxa:
<Ponter>=new <Tip> [Dimensiune];
Eliberarea memoriei alocate matricei se va realiza printr-o construcţie ce respectă sintaxa:
delete [ ] <Pointer>.
Sintaxa generală pentru declararea pointerilor la obiecte este:
<Pointer_la_obiect>=new <Clasa>;
Dacă <Clasa> are constructor parametrizat atunci se poate folosi sintaxa:
<Pointer_la_obiect>=new <Clasa>(<Lista_valori>);
pentru a realiza şi iniţializarea obiectului.

Alte aspecte importante:


1) Fiecare funcţie membră posedă un argument ascuns, numit this, argument transmis în mod automat
de către compilator. Aceasta variabilă (locală funcţiilor membru) reprezinta pointerul către obiectul
curent (cel care apelează metoda).
2) Obiectele dinamice nu se distrug automat, deoarece doar programatorul deţine informaţia despre
durata de viaţă a unui astfel de obiect.
3) Pentru operatii cu pointeri exista câteva reguli de sintaxă:
1. Pentru preluarea adresei unui membru al clasei se poate folosi operatorul ‘&’ urmat de numele
clasei, operatorul de rezoluţie şi numele membrului:
pointer_membru =& clasa :: membru;
2. Pentru accesul la un membru după preluarea adresei, trebuie indicat un obiect al clasei şi
numele variabilei pointer, folosind unul dintre operatorii specializaţi :
a. Operatorul .* dacă se specifică un obiect al clasei;
b. Operatorul ->* dacă se specifică un pointer de obiect.

Exemplul 2. Clasa Complex cu un constructor cu parametri, un constructor de copiere şi obiecte de tip


Complex definite în mod static.

Class Complex {
private:
float Re;
float Im;
public:
Complex (float a, float b); //constructor cu parametri
Complex (Complex & z); //constructor de initializare
// Alternativ putem defini un constructor cu lista de initializare
Complex :: Complex (float r, float i): Re(r), Im(i) { }
float get_parte_reala();
float get_parte_imaginara();
void set_parte_reala(float a);
void set_parte_imaginara(float b);
};
Complex :: Complex (float a, float b){ // definire in afara clasei,
utilizare this
this -> Re = a;

12
this -> Im = b;
}
Complex :: Complex (Complex & z){ // definire in afara clasei
Re = z.Re;
Im = z.Im;
}
// Codul altor functii membre este omis
int main(void){
Complex z1(2.0, 3.0);
Complex z2 = z1; // prin copiere – echivalent cu Complex z2(z1);
...cout << z1.get_parte_reala() << “+ i*” <<
z1.get_parte_imaginara();
...cout << z2.get_parte_reala() << “+ i*” <<
z2.get_parte_imaginara();
}

Exemplul 3. O nouă definiţie a clasei Persoana în care utilizăm alocare dinamică şi specificăm
destructorul. Deduceţi diagrama noii clase.

Class Persoana {
private:
char *nume;
int varsta;
public:
Persoana(char *n, int v){ // Constructor cu parametri definit
inline
nume = new char [strlen(n)+1];
strcpy(nume, n);
varsta = v;
}
Persoana(const Persoana &p){// Constructor de copiere definit
inline
nume = new char[strlen(p.nume)+1];
strcpy(nume, p.nume);
varsta = p.varsta;
}
~Persoana(){ //Destructor definit inline
delete nume;
}
void print(){ // Metoda definita inline
cout << "Numele persoanei:" << nume << endl;
cout << "Varsta persoanei:" << varsta << endl;
}
void set_Nume(char *n){ // Metoda definita inline
if(strlen(nume)<strlen(n)){
delete nume;
nume = new char [strlen(n)+1];
}
strcpy(nume, n);

13
}
void set_Varsta(int v){ // Metoda definita inline
varsta = v;
}
}; //Final definire clasă Persoana

Cea mai obişnuită formă de constructor de copiere este prezentată în exemplul de mai jos.

Exemplul 4. Un exemplu de clasă cu doi constructori şi destructor. Programul este compus din
funcţiile main şi afisare şi utilizează obiecte ale clasei definite.

#include <iostream>
using namespace std;
class Linie {
public:
int getLungime( void ); //Metoda GET
Linie(int lungime); // constructor uzual
Linie(const Linie &ob); // constructor de copiere
~Line(); // destructor
private:
int *adr;
};
Linie::Linie(int lung) {// primul constructor
adr = new int;
*adr = lung;
}
Linie::Linie(const Linie &ob) {
cout << "Constructor de copiere." << endl;
adr = new int;
*adr = *ob.adr; // copierea valorii
}
Linie::~Linie(void) { // Destructor
cout << "Eliberarea memoriei alocate de constructor!" << endl;
delete adr;
}
int Linie::getLungime(void) { // Metoda GET - implementare
return *adr;
}
void afisare(Linie ob) { // functie externa clasei
cout << "Lungimea liniei: " << ob.getLungime() << endl;
}

// Functia main pentru testare


int main(void) {
Linie linia1(10); afisare(linia1);
Linie linia2 = linia1; afisare(linia2);
return 0;
}

14
2.3. Clase derivate: definire, accesul la membrii clasei, constructorii şi destructorii în contextul
claselor derivate, tipuri de constructori.

În C++ putem deriva o clasă din alta clasă de bază sau din mai multe clase. În primul caz, sintaxa este
următoarea:
class Clasa_Derivată : [modificatori de acces] Clasa_de_Bază { ....
};

În <Lista_claselor_de_bază>, clasele care fac obiectul moştenirii/derivării, apar precedate, eventual,


de un modificator de protecţie şi sunt separate între ele prin virgulă:
class Clasa_Derivată : [modificatori de acces] Clasa_de_Bază1,
[modificatori de acces] Clasa_de_Bază2,
[modificatori de acces] Clasa_de_Bază3
{....};
Modificatorii de protecţie utilizaţi în <Lista_claselor_de_bază> definesc protecţia în clasa derivată a
elementelor moştenite. Dacă lipseste modificatorul de protecţie, atunci e considerat implicit private.

Notăm prin TACB – tipul de acces la clasa de bază, prin MPCB – modificatorul de protecţie asociat
clasei de bază la definirea noii clase, iar prin ACD – accesul la membrii clasei derivate. Sunt valabile
următoarele reguli:
R1: Dacă TACB = “private” şi MPCB = “private” atunci ACD = “interzis”;
R2: Dacă TACB = “protected” şi MPCB = “private” atunci ACD = “private”;
R3: Dacă TACB = “public” şi MPCB = “private” atunci ACD = “private”;
R4: Dacă TACB = “private” şi MPCB = “public” atunci ACD = “interzis”;
R5: Dacă TACB = “protected” şi MPCB = “public” atunci ACD = “protected”;
R6: Dacă TACB = “public” şi MPCB = “public” atunci ACD = “public”;
Deci: 1) clasa derivată are acces la elementele clasei de bază aflate sub incidenţa accesului
protected/public; 2) dacă la definirea clasei derivate se utilizează modificatorul de protecţie private,
atunci elementele protejate în clasa de bază prin protected sau public devin protejate private în clasa
derivată; deci inaccesibile unor clase care s-ar deriva eventual din clasa derivată; 3) Este posibil să se
moştenească o clasă de bază ca protected. Când se procedează astfel, toţi membrii public şi
protected ai clasei de bază devin membri protected ai clasei derivate.

Observaţii:
1. Prin moştenirea caracteristicilor este economisit timp de proiectare – implementare şi este
încurajată reutilizarea programelor care au fost temeinic testate anterior. Implementatorul
clasei specializate nu are nevoie decât de interfaţa clasei de bază şi de codul obiect al
implementării.
2. Un obiect al clasei specializate aparţine şi clasei de bază; invers nu este adevărat.
3. Este important să se facă distincţie între relaţiile “obiectul s este un obiect b” şi “obiectul s are
un obiect b”. Relaţia este se modelează prin specializare iar relaţia are prin compunere.
Relaţia “obiectul s este un b” se implementează prin moştenire; relaţia “obiectul s are un b” se
implementează prin agregare.
4. Orice pointer la un obiect al clasei specializate poate fi convertit la pointer către un obiect din
clasa de bază. Un pointer de o anumită clasă poate fi convertit în mod explicit la un pointer de
clasă specializată dacă valoarea sa referă un obiect al clasei specializate.
5. Crearea unei clase specializate nu afectează în nici un fel textul sursă sau codul obiect al clasei
(claselor, în cazul moştenirii multiple) de bază.

15
6. Modificările operate asupra unei clase server nu necesită schimbări ale claselor client, cu
condiţia ca aceste modificări să nu afecteze signatura interfeţei clasei server.
7. Există trei contexte în care o metodă client poate accesa un obiect server pentru a-i transmite
un mesaj:
a. serverul este o variabila locală a metodei client (cea mai simplă din punctual de
vedere al comunicării între obiecte: serverul este accesibil doar metodei client);
b. serverul este un atribut al clasei din care face parte metoda client (serverul este
accesibil tuturor metodelor clasei precum şi claselor şi funcţiilor prietene), context
întâlnit la reutilizarea codului;
c. serverul este un parametru al metodei client (serverul este accesibil metodei client dar
şi metodelor care au activat-o).
8. La compunere (agregare), constructorii din clasele membre se apelează înaintea
constructorului clasei ce conţine membrii. Constructorii şi destructorii sunt funcţii membre
care nu se moştenesc, întrucât aceştia posedă numele clasei. La creerea şi iniţializarea unui
obiect dintr-o clasă derivată se apelează implicit, mai întâi constructorii claselor de bază (în
ordinea în care apar în declaraţia clasei derivate) şi apoi constructorul clasei derivate.
9. La distrugerea unui obiect al clasei derivate, mai întâi este apelat destructorul clasei derivate,
şi apoi destructorii claselor de bază, în ordinea inversă celei în care apar în declaraţia clasei
derivate.

Figura 4. Exemplu de relaţie de specializare

Exemplul 5. Considerăm Clasa Student ca fiind derivată din clasa Persoana (din Exemplul 1, Fig. 3)
cu diagrama din Fig. 4. Codul C++ al clase Student este:
class Student:public Persoana{
public:
Student();
Student(char *nume, char *prenume, char *universitate, int
an_nastere=1984);
void afisare();
protected:
char *univ;
};
16
Se observă că în listele de iniţializare ale constructorilor Student este utilizat constructorul clasei
Persoana. Deoarece constructorul fără argumente Persoana()din lista de iniţializare a constructorului
fără parametri Student() pune anul de naştere la valoarea 2000, acesta trebuie modificat deoarece prin
lipsă, anul de naştere al unui student trebuie să fie 1984. De remarcat că acesta nu poate fi modificat
prin referire directă an=1984, deoarece acesta este declarat cu specificatorul private. El nu este
accesibil nici chiar clienţilor care specializează server-ul, cum este cazul aici. Modificarea anului este
însă posibilă prin metoda set_an_nastere(int), prevăzută special pentru asfel de situaţii. În
implementarea metodei Student::afisare()s-a utilizat metoda de afişare a clasei Persoana (prin
expresia Persoana::afisare() în care apare operatorul de rezoluţie ::)

Exemplul 6. Considerăm clasa Cerc definită prin centrul (Xc, Yc) şi raza R. Exemplificăm definirea
constructorilor supuşi supraîncărcării la nivelul clasei Cerc.

Class Cerc {
private:
double Xc;
double Yc;
double R;
public:
Cerc(){Xc = 0.0; Yc = 0.0; R = 1.0;} // cercul trigonometric
Cerc() Xc(10.0), Yc(10.0), R(100.0){} // constructor cu lista
Cerc(double xC, double yC, double raza){
Xc = xC;
Yc = yC;
R = raza;
} // constructor cu parametri
Cerc(Cerc & C){
Xc = C.Xc;
Yc = C.Yc;
R = C.R;
} // Constructor de copiere
void metoda1(); //definita mai tarziu folosind rezolutia
void metoda1 (double a, double b, double r); // supraincarcare
metoda1
double metoda2(); // definita mai tarziu folosind rezolutia
double metoda2(double r); //supraincarcarea metoda2
}

În POO, virtualizarea şi abstractizarea sunt tehnici de proiectare şi programare care fac diferenţa între
arhitecţii POO.

2.4. Metode virtuale şi clase abstracte.

În programarea orientată obiect, o funcţie virtuală sau o metodă virtuală este o funcţie sau o metodă a
cărei comportare poate fi redefinită într-o clasă derivată printr-o funcţie cu aceeaşi signatură. Acest
concept este o parte importantă a polimorfismului specific programării orientate obiect. Astfel, o
metodă virtuală este o funcţie declarată virtual în clasa de bază şi redefinită într-un lanţ de derivare
asociat respectivei clase de bază.

17
Clasele care conţin metode virtuale pure sunt denumite "abstracte" şi nu pot fi instanţiate direct. În
practica POO, o clasă abstractă este o clasă care este proiectată pentru a fi utilizată în mod explicit ca o
clasă de bază. O clasă care are ca membrii doar funcţii virtuale pure se numeşte interfaţă.

O funcţie virtuală pură este o funcţie care este declarătă în clasa de bază şi care impune clasei derivate
să o implementeze. Prototipul unei funcţii virtuale pure este:
virtual tip nume_functie(parametri)=0;
Constructorii nu pot fi funcţii virtuale. Destructorii pot fi virtualizaţi. Dacă un destructor nu este
declarat virtual atunci numai memoria alocată în clasa de bază va fi eliberată. Prin urmare, destructorul
clasei de bază fiind virtual, atunci vor fi apelaţi destructorii claselor derivate.

Exemplul 7. Virtualizare şi abstractizare folosind forme geometrice. Să presupunem că avem mai


multe clase care reprezintă forme geometrice: Punct, Cerc, Cilindru, derivate din clasa de bază Figura.
Fiecare dintre aceste clase trebuie să permită afişarea numelui formei pe care o reprezintă şi alte
informaţii specifice precum coordonate, raza, înalţime, arie, volum.

class Figura{
public:
virtual double aria() const {return 0.0;}
virtual double volumul() const {return 0.0;}
//functii virtuale pure suprascrise in clasele derivate
virtual void AfisareNume() const = 0;
virtual void Afisare() const = 0;
};
class Punct : public Figura{
public:
Punct() x(0), y(0) {}; //constructor implicit cu lista de
initializare
Punct (int x = 0, int y = 0);
void setPunct(int, int); //fixeaza coordonatele
int getX() const { return x; } //inline - obtine abscisa x
int getY() const { return y; } //inline - obtine ordonata y
virtual void afisareNume() const {cout << "Punct: ";} //inline
virtual void afisare() const;
private:
int x, y; //x si y sunt coordonatele punctului
};
Punct::Punct(int a, int b) {setPunct(a, b);}
void Punct::setPunct(int a, int b){x = a; y = b;}
void Punct::afisare() const {cout << '(' << x << ", " << y << ')'; }
class Cerc : public Punct{
public:
Cerc(double r = 0.0, int x = 0, int y = 0); //constructor
implicit
void setRaza(double); //fixeaza raza
double getRaza() const; //obtine raza
virtual double aria() const; //calculeaza aria
virtual void afisareNume() const {cout << "Cerc: ";}
virtual void afisare() const;

18
private:
double r;
};
Cerc::Cerc(double r, int a, int b) : Punct(a, b){ setRaza(r); }
void Cerc::setRaza(double raza){ r = (raza > 0 ? raza : 0);}
double Cerc::getRaza() const { return r; }
double Cerc::aria() const {return 3.14159*r*r; }
void Cerc::afisare() const{
Punct::afisare();
cout << "; Raza = " << r;
}
class Cilindru : public Cerc {
public:
Cilindru(double h = 0.0, double r = 0.0, int x = 0, int y = 0);
void setH(double);
double getH() const;
virtual double aria() const;
virtual double volumul() const;
virtual void afisareNume() const {cout << "Cilindru: ";}
virtual void afisare() const;
private:
double h;
};
Cilindru::Cilindru(double h, double r, int x, int y) : Cerc(r, x,
y){setH(h);}
void Cilindru::setH(double a){ h = (a >= 0 ? a : 0); }
double Cilindru::getH() const {return h;}
double Cilindru::aria() const{ return 2*Cerc::aria()+2*3.14159*r*h;}
double Cilindru::volumul() const {return Cerc::aria()*h;}
void Cilindru::afisare() const{Cerc::afisare(); cout << "; Inaltimea
= " << h;}

2.5. Supraîncărcarea metodelor

Polimorfismul este capacitatea unor entităţi de a lua forme diferite. În C++ se întâlnesc două forme de
poliformism:
1) Polimorfismul parametric – mecanismul prin care putem defini o metodă cu acelaşi nume în
aceaşi clasă (am văzut constructori pâna acum, dar este valabil pentru orice metode cu
excepţia destructorilor), funcţii care trebuie să difere prin numărul şi / sau tipul parametrilor.
Selecţia funcţiei se realizează la compilate (legarea timpurie (early binging)).
2) Polimorfismul de moştenire – mecanismul prin care o metodă din clasa de bază este redefinită
cu aceiaşi parametri în clasele derivate. Selecţia funcţiei se va realiza la executare (legarea
întârziată (late binding, dynamic binding, runtime binding)). În limbajul C++ este implementat
cu ajutorul funcţiilor virtuale

În C++ nu numai metodele (funcţtile membre ale claselor), dar şi funcţiile specifice programării
procedurale se pot supraîncărcă, cu excepţia funcţiei main. Este esenţial ca numărul şi / sau natura
parametrilor să difere (să aibă signaturi diferite).

19
Exemplul 8. Suma elementelor unui tablou se calculează folosind acelaşi algoritm. Dacă tipul datelor
diferă, atunci operaţiile aritmetice diferă şi deci se vor utiliza componente diferite ale prcesorului.
Funcţia sum este implementată atât pentru numere întregi, cât şi pentru numere în virgulă mobilă.

int sum(int * tab, int nr_elemente){


int rez = 0;
int k;
for (k = 0; k < nr_elemente; k++) rez += tab[k];
return rez;
}
double sum(double * tab, int nr_elemente){
double rez = 0.0;
int k;
for (k = 0; k < nr_elemente; k++) rez += tab[k];
return rez;
}
int main(){ //exemplu de utilizare
int x[10]={1,2,3,4,5,6,7,8,9,10};
double y[3]={3.14,2.71,6.28};
cout << "suma valorilor int este " << sum(x,10 )<< endl;
cout << "suma valorilor double este " << sum(y,3) << endl;
return 0;
}

Exemplul 9. Aici este creată o clasă fără explicitarea constructorilor şi a destructorului. În plus,
metoda sch este supraîncărcată. Studiaţi şi executaţi codul de mai jos. Reimplementaţi folosind
programare generică (vedeţi secţiunea 2.10).

#include <iostream>
class util{
public:
void sch(int *a, int *b) {int temp; temp = *a; *a = *b; *b =
temp;}
void sch(float *a, float *b) {float temp; temp = *a; *a = *b; *b
= temp;}
void sch(double *a, double *b) {double temp; temp = *a; *a = *b;
*b = temp;}
};
int main(){
int a1 = 640, b1 =480;
float a2 = 1.41, b2 = 1.73;
double a3 = 3.14, b3 = 12.71;
util m;
std::cout << a1 << ", " << b1 << std::endl;
m.sch(&a1, &b1);
std::cout << a1 << ", " << b1 << std::endl;
std::cout << a2 << ", " << b2 << std::endl;
m.sch(&a2, &b2);
std::cout << a2 << ", " << b2 << std::endl;

20
std::cout << a3 << ", " << b3 << std::endl;
m.sch(&a3, &b3);
std::cout << a3 << ", " << b3 << std::endl;
return 0;
}

Exemple privind polimorfismul parametric au fost întâlnite la constructorii din sectiunile anterioare.
Polimorfismul de moştenire a fost arătat prin exemplul 7 în cadrul unei ierarhii de clase. Alte exemple
de supraîncărcare vor fi prezentate în secţiunea 2.7.

2.6. Metode inline, funcţii friend

Prin inline specificăm compilatorului necesitatea substituirii codului funcţiei oriunde îi apare numele.
Astfel, la apelul funcţiilor compilatorul decide dacă funcţia este substituită sau este un subprogram.

Considerăm codul:

inline int aduna(int x, int y) { return x+y; }


int calc(){
int p =3; int q=5;
int rez = add(p, q); // aici va avea loc substituire
return rez;
}

Atunci linia int rez = aduna(p, q); va fi îlocuită cu int rez = p+q;

Toate funcţiile membre definite în interiorul unei clase sunt automat inline, chiar dacă nu este folosit
cuvântul cheie ca specificator, precum în secvenţa de cod:

class Calculator {
private:
int rez;
public:
int getRez() const {
return rez;
}
}; // atunci când codul metodei este foarte scurt

Cuvântul cheie inline poate fi folosit şi în afara clasei atunci când codul este scurt:

class Calculator {
private:
int rez;
public:
int getRez() const;
}
inline int Calculator :: getRez() const {return rez;}

21
O funcţie prietenă (friend) a unei clase este definită în afara domeniului acestei clase, dar are dreptul
de a accesa toţi membrii privaţi şi protejaţi ai clasei. Chiar dacă prototipurile pentru funcţiile friend
apar în definiţia clasei, prietenii nu sunt funcţii membre. O entitate friend poate fi o funcţie, un şablon
de funcţie sau o funcţie membru, sau o clasă sau un şablon de clasă, caz în care întreaga clasă şi toţi
membrii săi sunt de tip friend.

Pentru a declara o funcţie friend pentru o clasă, prototipul funcţiei din definiţia clasei trebuie precedat
de cuvântul cheie friend:

class A { ... alte elemente ale clasei


friend <tip> <identificator_functie>([Lista-de-argumente]);
};

Declarea unei familii de clase care să aibă acces la entităţile clasei A respectă sintaxa:

class A { ... alte elemente ale clasei


friend class lista_claselor_separate_prin_virgula;
};

Exemplul 10. Functia suma afişează rezultatul adunării numerelor stocate de obiectele din clasele A şi
B. Prin executarea codului de mai jos se va obţine valoarea 1120 (= 640 + 480).

#include <iostream>
using namespace std;
// declaratie forward a clasei B
class B;
class A {
private:
int x;
public:
A(): x(640) { } //constructor cu lista de initializare
friend int suma(A, B); // functie friend pentru clasele A si B
};

class B {
private:
int y;
public:
B(): y(480) { }//constructor cu lista de initializare
friend int suma(A , B); //aici argumentele sunt obiecte
};

int suma(A x, B y){return (x.x + y.y);}


int main(void){
A a;
B b;
Cout << "Suma: "<< suma(a, b);
return 0;
}

22
Exemplul 11. Clasa B este prietenă cu clasa A. Invers nu este valabil.

#include <iostream>
using namespace std;
class A{
private:
double xa;
double ya;
public:
A(double x, double y){xa = x; ya = y;}
friend class B; //poate fi acordat accesul doar metodei print
};
class B{
private:
int p;
public:
B(int q) {p = q;}
void print(A &a){
double t;
switch(p){
case 1: { t = (a.xa + a.ya)/2.0; // media aritmetica
std::cout << t << '\n'; break;
}
case 2: { t = (a.xa > a.ya)? a.xa : a.ya; // maximul
std::cout << t << '\n'; break;
}
} //sf. switch
} // sf. print
};

int main(){
A a(640.00, 480.00);
B b(1), c(2);
b.print(a); //media aritmetica
c.print(a); // maximul
return 0;
}

În loc de a defini prietenia pentru întreaga clasă putem defini prietenia doar pentru o anumită metodă a
clasei, caz în care folosim rezoluţia:

friend B:: void print (A &a);

2.7. Supraîncărcarea operatorilor

Polimorfismul este realizat în C++ şi prin supraîncărcarea operatorilor, o tehnică de programare foarte
interesantă şi cu implicaţii practice. Când un operator este supraîncărcat, el capătă o semnificaţie
suplimentară relativ la o anumită clasă fără să-şi piardă vreunul din înţelesurile iniţiale. Mai sus au fost

23
utilizaţi operatorii << şi >> supraîncărcaţi în fişierul iostream.h, care acţionează, de obicei ca operatori
de deplasare.

Operatorii aritmetici precum + şi / sunt deja supraîncărcaţi în C / C ++ pentru diferite tipuri


încorporate. De exemplu 1/3 == 0, iar 1.0/3 == 0.3333. Operatorul = şi operatorul & sunt
supraîncărcaţi implicit pentru fiecare clasă, astfel încât acestea pot fi folosite pentru fiecare obiect de
clasă.
• operatorul = efectuează o copie a datelor membre.
• operatorul & returnează adresa obiectului în memorie.

Prin supraîncărcarea operatorilor nu se poate schimba aritatea (signatura), prioritatea sau


asociativitatea operatorilor, acestea fiind elemente predefinite în limbaj.

Supraîncărcarea operatorilor se realizează cu ajutorul unor funcţii membre sau prietene (friend).
Numele acestor funcţii se compune din cuvântul cheie operator şi unul sau mai multe caractere care
definesc operatorul care se supraîncarcă. Pentru operatorii new şi delete, trebuie să existe cel puţin un
spaţiu între cuvântul cheie operator şi caracterele care definesc operatorul de supraîncărcat.

Deci, orice metodă al cărei nume este de forma: operator<operator_C ++> se spune că supraîncărcă
operatorul specificat. Când se supraîncarcă un operator, compilatorul va apela funcţia în loc să
interpreteze operatorul în mod normal. Sunt supraîncărcabili următorii operatori C++:

+ - * / % ^ & |
~ ! , = < > <= >=
++ -- << >> == != && ||
+= -= /= %= ^= & = |= *=
<<= >>= [ ] ( ) -> ->* new, new []
delete, delete []

Operatorii C++ care nu pot fi încărcaţi sunt: . (selecţia), :: (rezoluţia), sizeof, typeid, dereferenţierea .*
(pointer la membrul clasei) şi ?: (decizia). Felul în care sunt scrise funcţiile operator diferă pentru cele
de tip membru de cele de tip friend.

2.7.1. Supraîncărcarea cu funcţii operator membre

Funcţiile operator membru au sintaxa de implementare:


<Tip_returnat> <Nume_clasă>::operator# (<Lista_argumente>)
{//Operaţii specifice }
unde, # specifică caracterele ce definesc operatorul C++ care se supraîncarcă. Funcţiile operator
membre au un singur parametru sau nici unul. În cazul în care au un parametru, acesta se referă la
operandul din dreapta al operatorului. Celălalt operand ajunge la operator prin intermediul pointerului
special this.

Observaţie. Nu se pot utiliza funcţii operator membru pentru a supraîncărca un operator dacă
operandul stâng nu este un obiect al clasei respective. În acest caz trebuie utilizate funcţii friend
operator.

Exemplul 12. Supraîncărcarea operatorului + pentru adunarea a două numere complexe definite ca
obiecte ale clasei my_complex.

24
#include<iostream>
using namespace std;
class my_complex {
private:
double re;
double im;
public:
my_complex (double a, double b){ re = a; im = b;};
void afisare();
my_complex operator+(my_complex &op);
};
void my_complex :: afisare(){ cout << re << " +i* " << im << endl;};
my_complex my_complex :: operator+(my_complex &op){
my_complex t(0.0, 0.0);
t.re = this->re + op.re;
t.im = this->im + op.im;
return t;
};
int main(void){
my_complex t1(0.0,0.0), t2(0.0,0.0), *tt1, *tt2;
my_complex z1(2.0, 3.0), z2(7.0, 5.0);
z1.afisare(); z2.afisare();
z1 = z1 + z2; // testati si z1 = z1.operator+(z2);
z1.afisare();
tt1 = new my_complex (3.00, 11.00);
tt2 = new my_complex (5.0, 2.0);
t1 = *tt1; t1.afisare();
t2 = *tt2; t2.afisare();
t1 = t1 + t2; t1.afisare();
return 0;
}

Exemplul 13. Extindem exemplul 12 pentru a suporta şi alte operaţii aritmetice, precum şi verificările
relaţionale == şi != definind tipul a_complex.

#include<iostream>
using namespace std;
class a_complex {
private:
double re;
double im;
public:
a_complex();
a_complex(double);
a_complex(double, double);
a_complex operator+(const a_complex &op) const;
a_complex operator-(const a_Complex &op) const;
a_complex operator*(const a_Complex &op) const;

25
a_complex operator/(const a_complex &op) const;
bool operator==(const a_complex &op) const;
bool operator!=(const a_complex &op) const;
void a_afisare();
};
a_complex::a_complex() {re = im = 0.0;}; //implicit
a_complex::a_complex(double r) {re = r; im = 0.0;}; //nr reale
a_complex a_complex::operator+(const a_complex &u) const{
a_complex v(re + u.re, im + u.im);
return v;
}
a_complex a_complex::operator-(const a_complex &u) const{
a_complex v(re - u.re, im - u.im);
return v;
}
a_complex a_complex::operator*(const a_complex &u) const{
a_complex v( re * u.re - im * u.im, im * u.re + re * u.im);
return v;
}
a_complex a_complex::operator/(const a_complex &u) const{
double abs_pt = re * u.re + im * u.im;
a_complex v( ( re * u.re + im * u.im) / abs_pt,(im * u.re - re *
u.im ) / abs_pt );
return v;
}
bool a_complex::operator==(const a_complex &u) const{
return (re == u.re && im == u.im) ;
}
bool a_complex::operator!=(const a_complex &u) const{
return !(re == u.re && im == u.im) ;
}
void a_complex :: afisare(){ cout << re << " + " << im << "*i" <<
endl;};

2.7.2. Supraîncărcarea cu funcţii operator de tip friend

Deoarece o funcţie friend nu este membru al clasei, nu are disponibil un pointer de tip this. De aceea,
unei funcţii supraîncărcate de tip friend operator i se vor transmite explicit operanzii. Nu putem
folosi funcţiile friend pentru supraîncărcarea operatorilor = [ ] ( ) şi ->.

Sintaxa prototipului unei funcţii friend operator este:


friend < tip rezultat > operator# ( <operand1>& , <operand2> &) ;,
precum în descrierea friend my_complex operator+ (my_complex &, my_complex &);

Exemplul 14. Supraîncărcare folosind funcţii friend. Este ilustrat transferul operandului prin valoare,
respectiv prin referinţă în cazul supraîncărcării operatorului unar – aplicat pentru numere complexe
(sau puncte din plan).

26
Transfer prin valoare Transfer prin referinţă
Cod C++ #include <iostream> #include <iostream>
using namespace std; using namespace std;
class X{ class X{
double re, im; double re, im;
public: public:
X(){re = im = 0.0;}; X(){re = im = 0.0;};
X (double r, double X (double r, double i) {
i) re = r; im =i;};
{re = r; im void afisare();
=i;}; friend X operator- (X &);
void afisare(); };
friend X operator- X operator- (X &z){
(X); z.re = -z.re;
}; z.im = -z.im;
X operator- (X z){ return z;}
z.re = -z.re; void X :: afisare(){
z.im = -z.im; cout << re << " +i* "
return z;} << im << endl;
void X :: afisare(){ }
cout << re << " +i* int main (void) {
" X z1(2.0, 3.0);
<< im << endl; z1.afisare();
} - z1;
int main (void) { z1.afisare(); //z1 modificat
X z1(2.0, 3.0), z2; return 0;
z1.afisare(); }
z2 = - z1;
z2.afisare();
// z1 este
nemodificat
return 0;
}
Rezultatul 2 +i* 3 2 +i* 3
executării -2 +i* -3 -2 +i* -3

2.8. Fluxuri de intrare-ieşire în C++

C++ defineşte propriul său sistem I/O orientat pe obiecte. Sistemul de fişiere C/C++ recunoaşte două
tipuri de fluxuri:text şi binar. Un flux de tip text este o secvenţă de caractere. Un flux binar este o
secvenţă de octeţi. Un flux se asociază cu un anumit fişier printr-o operaţie de deschidere. Odată
deschis fişierul, este posibil schimbul de date între el şi programul aflat în executare. Operaţiile cu
fluxuri sunt posibile prin utilizarea claselor din iostream.h (inclusiv obiectele cin din clasa istream,
cout şi cerr, ambele din clasa ostream). Inserarea în flux se realizează folosind operatorul
(supraîncărcat) <<, iar extragerea din flux foloseşte operatorul >>.
Metodele pentru formatarea ieşirii sunt:
long setf (long indicator);//activarea indicatorilor de format
long unsetf(long indicator);
int width (int w);

27
int precision (int p);
char fill (char ch);
Formatarea se mai poate realiza şi cu ajutorul manipulatorilor: dec, hex, oct, endl,
flush, setfill(int), setprecision(int) şi setw(int).

Operaţiile cu fişiere se pot programa folosind clasele din fstream.h. Operaţiile de intrare / ieşire cu
fişiere se fac prin intermediul claselor ifstream, ofstream şi fstream. Constructorii acestor clase sunt:

ofstream::ofstream(const char* nume,ios::openmode= ios::out| ios::trunc);


ifstream::ifstream(const char* nume, ios::openmode= ios::in);
fstream::fstream(const char* nume, ios::openmode= ios::in | ios::out);

Modurile de deschidere a fişierelor sunt:


 ios::app - adauga la sfârşitul fişierului;
 ios::ate - poziţionează pointer-ul la sfârşitul fişierului, însă informaţiile pot fi scrise
oriunde în cadrul fişierului;
 ios::trunc - este modul de deschidere implicit: vechiul conţinut al fişierului este pierdut;
 ios::binary –deschide fişierul în mod binar;
 ios::in – deschidere pentru operaţii de intrare;
 ios::out – deschidere pentru operaţii de ieşire.

Rezultatul operaţiilor de intrare / ieşire poate fi testat prin intermediul a patru funcţii membre:
 eof() - verifică dacă s-a ajuns la sfârşitul fişierului;
 bad() - verifică dacă s-a executat o operaţie invalidă;
 fail() – verifică dacă ultima operaţie a eşuat;
 good() – verifică dacă toate cele trei rezultate precedente sunt false.
Închiderea unui fişierse face apelând metoda close().

Scrierea într-un fişier binar se face cu funcţia:


ostream& write (const char* s , size n);
Citirea dintr-un fişie rbinar se face cu funcţia:
istream& read(const char* s , size n );
Obţinerea poziţiei într-un fişier se face cu:
 pos_type ostream::tellp();
 pos_type istream::tellg();
Poziţionarea într-un fişier se face cu:
 ostream&ostream::seekp(off_type offset,ios::seekdir pos);
 istream&istream::seekg(off_type offset,ios::seekdir pos);
unde pos poate avea una din următoarele valori:
 ios::beg – specifică poziţia de la începutul fluxului;
 ios::end – specifică poziţia de la sfârşitul fluxului;
 ios::cur – specifică poziţia curentă a fluxului;

Exemplul 15. Citirea dintr-un fişier text şi scriere în alt fişier text după efectuarea unei transformări.

#include<iostream>
#include<fstream>
#include <string.h>
using namespace std;
28
int main(void){
int k=0;
int x[100];
int i=0;
fstream f("fis.in",ios::in);
fstream g("fis.out",ios::out);
while(!feof()&& k<100){f >> {x[k]; k++;}
// primele 100 de numerele (daca sunt atatea) din fis.in se depun
in tabloul x
f.close();
for (i=0; i<k; i++) x[i] = x[i]+1; // se inlocuieste cu
succesorul
for (i=0; i<k; i++) g << x[i];
g.close();
fstream g("fis.out",ios::app);
g << 666; //se adauga 666 la final.
g.close();
return 0;
}

2.9. Tratarea excepţiilor

Prin programare defensivă se vor crea programe robuste care tratează erorile (excepţiile) care apar în
timpul executării. Pentru aceasta în C++ mai sunt disponibile cuvintele cheie try, throw şi catch.
Programul plasează instrucţiunea throw în cadrul blocului try-catch. Forma generalizată a blocului
care captează şi tratează erorile este:
try{
//blocul try
//if(eroare) throw valoare_excepţie;
}catch (Tip_excepţie Nume_variabilă ){ //Prelucrarea excepţiei
}

Exemplul 16. Programul verifică dacă s-a depasit capacitatea rablului x.

#include<iostream>
using namespace std;
int main () {
char x[10];
try{
for (int n=0; n<=10; n++){
if (n>9) throw "Depasire";
x[n]='z';
}
} catch (char * str){
cout << "Exceptie: " << str << endl;
}
return 0;
}

29
2.10. Programare generică în C++. Biblioteca standard de şabloane.

Programarea generică este un stil de programare a calculatoarelor în care algoritmii sunt specificaţi în
termeni de tipuri ce urmează a fi precizate în momentul apelului (utilizării). Astfel, o funcţie generică
defineşte un set general de operaţii care vor fi aplicate unor tipuri de date diferite. În C++ există suport
evoluat pentru programare generică, sub forma şabloanelor (templates). O funcţie template este o
funcţie şablon, având unul sau mai mulţi parametri formali de un tip generic. La utilizare, compilatorul
generează funcţii propriu-zise, înlocuind tipul generic cu un tip concret. Tipul concret poate fi orice tip
fundamental, derivat sau clasă predefinită. Sintaxa la specificare este:
template <class Nume_tip_generic_1 [,…class Nume_tip_generic_n]>

Sintaxa la utilizare este:


Nume şablon<Expresie_1[, …,Expresie_n]>;
În scrierile de mai sus, caracterele < şi > fac parte din sintaxa obligatorie. Următoarea secvenţă
specifică funcţia şablon max:
template <class T> T max(T x, T y) { return (x>y) ? x : y; }

O clasă template defineşte un şablon pe baza căruia se pot genera clase propriu-zise. Din acest motiv,
o clasă template se mai numeşte şi clasă generică , clasă generator sau metaclasă. Astfel, o clasă
template devine o clasă de clase, reprezentând cel mai înalt nivel de abstractizare admis de
programarea orientată pe obiecte.

Sintaxa la specificarea clasei este:


template <class T> class nume_clasă { …………….};
Sintaxa la implementarea funcţiilor membre ale unei clase template este:
template <class T> Tip returnat nume_clasa <T>::nume_funcţie(…) {
………};

Exemplul 17. Tablouri generice.

#include <iostream>
using namespace std;
const int DIM = 100;
//***************************************
//Definire clasa tablou generic
template <class A> class genTAB {
A a[DIM];
public:
genTAB(); //constructor
A &operator[](int k); //supraincarcare operator [ ]
};
//implementare constructor clasă generica
template <class A> genTAB<A>::genTAB() {
int k;
for(k=0; k<DIM; k++) a[k]=k*k;
};
//implementare supraincarcare operator []
template <class A> A &genTAB<A>::operator[](int k) {

30
if(k<0 ||k>= DIM){
cout << "Index in afara domeniului admis.";
exit(1);
};
return a[k];
};
//testare
int main(void) {
genTAB<int> intA;
genTAB<double> doubleA;
int k;
cout<<"Tabloul patratelor numerelor" << endl;
for(k=0; k<DIM; k++) intA[k]=k*k;
for(k=0; k<DIM; k++) cout << k << intA[k] << endl;
cout << "Tabloul optimilor indicilor" << endl;
for(k=0; k<DIM; k++) doubleA[k]=(double)k/8;
for(k=0; k<DIM; k++) cout << k << doubleA[k] << endl;
intA[DIM]=DIM;
};

Biblioteca standard C++ are trei mari componente: biblioteca de funcţii C, cea de fluxuri de intrare /
ieşire şi Standard Template Library (STL) (Biblioteca standard de şabloane). STL are la bază
polimorfismul parametric şi trei concepte centrale: containeri, iteratori şi algoritmi.

Un container este un obiect care stochează o colecție de alte obiecte (elementele sale). Un container
poate fi definit ca fiind o colecţie de date care suportă cel puţin următoarele operaţii: adăugarea unui
element în container; ştergerea unui element din container; returnarea numărului de elemente
din container (dimensiunea containerului); permite accesul la obiectele stocate (de obicei folosind
iteratori) şi permite căutarea unui obiect în container.

Containerii sunt implemetaţi drept clase şablon şi se referă la cele mai importante structuri de date:
tablouri dinamice (vector), cozi (queue), stive (stack), heap-uri (priority_queue), liste înlănţuite (list),
mulţimi (set), tablouri asociative (map), etc.

Iteratorii sunt o generalizare a referinţelor şi anume ca obiecte care referă alte obiecte. Iteratorii sunt
des utilizaţi pentru a parcurge un container de obiecte. Iteratorul se mai numeşte şi cursor. Iteratorii
pot fi incrementaţi cu ++, dereferenţiaţi cu * şi comparaţi cu !=. Containerele pot genera iteratori cu
funcţiile begin() şi end().

Iteratorii pot fi împărţiţi în categorii de iteratori, în funcţie de operaţiile care se pot efectua asupra lor.
(iteratori de intrare, iteratori de ieşire, iteratori de avans, iteratori bidirecţionali şi iteratori cu acces
direct).

Algoritmii STL sunt generici deoarece se pot aplica la o mare varietate de structuri de date. Antetul
<algorithm> definește o colecție de funcții special concepute pentru a fi utilizate pe subşiruri de
elemente.

Exemplul 18. Ordonarea descrescătoare a unui şir de numere folosind elemente STL.

31
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int x[] = {10, 17, 20, -5, 14, 78, 11, 61, 25} ;
sort( x, x+9, greater<int>());
for (int k=0 ; k<9 ; k++) cout << x[k] << " ";
return 0;
}

Tematica asupra STL va fi extinsă în altă lucrare. Cititorii interesaţi pot consulta lucrarea [5].

1. Implementarea algoritmilor în C++: aplicaţii cu numere, caractere, şiruri de caractere, tablouri,


liste, grafuri, dicţionare.

Exemplul 19 [Aplicatie cu numere]. Se citesc L fracţii dintr-un fişier text numit fractii.in. Să se
afişeze numai fracţiile reductibile ordonate descrescător. Modificaţi implementarea pentru a înlocui
struct cu class.

#include <iostream>
#include <fstream.h>
using namespace std;
struct fract {int p,q;};
ifstream fin("fractii.in");
void read(fract &f){fin >> f.p >> f.q;}
void print(fract f){cout << f.p << "/" << f.q << "#";}
void sortSCH(int m,fract *x){ //sortare prin interschimbare
int k, ok; fract temp;
do{
ok=1; //sirul este sortat
for(k=0;k<m;k++)
if (x[k].p * x[k+1].q < x[k].q*x[k+1].p){
temp = x[k]; x[k] = x[k+1];
x[k+1]=temp; ok=0;
}
}while (!OK);
}
int cmmdc(int a,int b) {while(a!=b) if (a>b) a=a-b; else b=b-a;
return a;}
int main(void) {int k, L; fract x[100]; //presupunem ca nu exista
mai mult de 100
f >> L;
for (k=0; k<L; k++) read(x[k]);
sortSCH(L,x);
for (k=0;k<L;k++) if(cmmdc(x[k].p,x[k].q)!=1) print (x[k]);
//filtrarea fractiilor
f.close();
return 0;
}

32
Exemplul 20 [Aplicaţie cu caractere şi şiruri de caractere]. şirul de caractere este o colecție de
caractere. Există două tipuri de șiruri de caractere utilizate în mod obișnuit în limbajul de programare
C ++: obiecte ale clasei String (în C++), respectiv tablouri de caractere conform specificaţiilor
string.h din limbajul C. Accesul în C++ la caracterul de pe poziţia k din şirul text este
text[k]. Numărul de caractere ale obiectului text este text.size.

#include <iostream>
using namespace std;
int main(void){
// Declararea unui obiect string
string text;
cout << "Introduceti textul: ";
getline(cin, text);
cout << "A fost introdus: " << text << endl;
return 0;
}

Exemplul 21 [Aplicaţie cu tablouri bidimensionale]. Scrieți un program pentru a proiecta o clasă


pentru lucrul cu matrice. Clasa ar trebui să aibă funcționalitatea de a insera (upload) și de a prelua
(download) elementele matricei.

#include<iostream.h>
class MAT {
int **p; //pointer – alocare dinamica a spatiului
int dim1, dim2;
public:
MAT(int x, int y);
void upload(int i, int j, int val) {
p[i][j]=value;
}
int & download(int i, int j) {
return p[i][j];
}
};
MAT ::MAT(int m, int n) {
dim1 = m;
dim2 = n;
p = new int *[dim1];
for(int k = 0; k < dim1; k++)
p[k] = new int[dim2];
}
int main() {
int m, n;
cout<<"Introduceti dimensiunile matricei";
cin >> m >> n;
MAT A(m,n);
Cout << "Dati elementele matricei pe linii:";
int i,j,val;

33
for(i=0; i<m; i++)
for(j=0;j<n;j++){
cin >> val;
A.upload(i,j,value);
}
Cout << "\n";
Cout << A.download(2,3);
return 0;
}

Exemplul 22 [Aplicaţie cu liste]. Se va crea clasă Lista pentru manipularea listelor înlănţuite şi se va
folosi în funcţia main..

#include <iostream>
#include <cstlib>
using namespace std;

class Nod{
public:
Nod* leg;
int data;
};
class Lista{
public:
int lung;
Nod* h; //capul listei
Lista();
~Lista(); //destructor
void adauga(int data);
void afisare();
};
Lista::Lista(){
this->lung = 0;
this->h = NULL;
}
Lista::~Lista(){
cout << "Se elibereaza memoria";} //Completati codul
void Lista::adauga(int data){
Nod* nod = new Nod();
nod->data = data;
nod->leg = this->h;
this->h = nod;
this->lung++;
}
void Lista::afisare(){
Nod* p = this->h;
int k = 1;
while(p){
cout << i << ": " << p->data << endl;

34
p = p->leg;
k++;
}
}

int main(){
Lista* L = new Lista();
for (int k = 0; k < 100; ++k){
list->adauga(rand() % 6);//numere generate aleator
}
L->afisare();
cout << "Numarul de elemente din lista: " <<
L->lung << endl;
delete L; // Ce spatiu se elibereaza?
return 0;
}

Exemplul 23 [Aplicaţie cu grafuri]. Următorul program verifică conexitatea unui graf neorientat
folosind căutarea în adâncime (eng. DFS – depth first search)

#include <iostream>
#include <list> // din STL
#include <stack> // din STL
using namespace std;
class Graf{
private:
int V; // varfuri
list<int> *lad; //lista de adiacenta
void DFS(int v, bool vizitat[]);
public:
Graph(int V){ // constructor
this->V = V;
lad = new list<int>[V];
}
~Graph(){ //destructor
delete [] lad;
}
void muchie(int v, int w);
bool esteconex();
Graf Transpus();
};
void Graf::DFS(int v, bool vizitat[]){
vizitat[v] = true;
list<int>::iterator k;
for (k = lad[v].begin(); k != lad[v].end(); ++k)
if (!vizitat[*k]) DFS(*k, vizitat);
}
Graf Graf::Transpus(){
Graf g(V);

35
for (int v = 0; v < V; v++){
list<int>::iterator k;
for(k = lad[v].begin(); k != lad[v].end();
++k)g.lad[*k].push_back(v);
}
return g;
}
void Graf::muchie(int v, int w){
lad[v].push_back(w);
lad[w].push_back(v);
}
bool Graf::esteconex(){
bool vizitat[V];
for (int v = 0; v < V; v++) vizitat[v] = false;
DFS(0, vizitat);
for (int v = 0; v < V; v++) if (vizitat[v] == false) return false;
Graf gt = Transpus();
for(int v = 0; v < V; v++) vizitat[v] = false;
gt.DFS(0, vizitat);
for (int v = 0; v < V; v++) if (vizitat[v] == false) return false;
return true;
}
int main(void){
Graph g(5);
g.muchie(0, 1); g.muchie(1, 2); g.muchie(2, 3); g.muchie(3, 0);
g.muchie(2, 4);
g.muchie(4, 2);
if (g.esteconex()) cout <<"Graful este conex" << endl; else
cout << "Graful nu este conex" << endl;
return 0; // adaptare dupa [9]
}

Exemplul 24 [Aplicaţie cu dicţionare]. Dictionarul este organizat sub forma unui arbore de cautare

#include<iostream>
#include<stdlib.h>
using namespace std;
#define maxim 100
typedef struct lista{
int data;
struct lista *leg;
} tipnod;
tipnod *pointer[maxim], *radacina[maxim], *temporar[maxim];
class Dictionar{
public:
int index;
Dictionar(); //constructor
void inserare(int);
void cautare(int);

36
void stergere(int);
};
Dictionar::Dictionar(){
index = -1;
for (int k = 0; k < maxim; k++){
radacina[k] = NULL;
pointer[k] = NULL;
temporar[k] = NULL;
}
}
void Dictionar::inserare(int cheie){
index = int(cheie % maxim);
pointer[index] = (tipnod*) malloc(sizeof(tipnod));
pointer[index]->data = cheie;
if (radacina[index] == NULL){
radacina[index] = pointer[index];
radacina[index]->leg = NULL;
temporar[index] = pointer[index];
}
else{
temporar[index] = radacina[index];
while (temporar[index]->leg != NULL)
temporar[index] = temporar[index]->leg;
temporar[index]->leg = pointer[index];
}
}
void Dictionar::cautare(int cheie) {
int ok = 0;
index = int(cheie % maxim);
temporar[index] = radacina[index];
while (temporar[index] != NULL){
if (temporar[index]->data == cheie){
cout << "\nCheie gasita"; ok = 1; break;}
else temporar[index] = temporar[index]->leg;
}
if (!ok) cout << "\nCheia nu a fost gasita";
}
void Dictionar::stergere(int cheie){
index = int(cheie % maxim);
temporar[index] = radacina[index];
while (temporar[index]->data != cheie && temporar[index] !=
NULL){
pointer[index] = temporar[index];
temporar[index] = temporar[index]->leg;
}
pointer[index]->leg = temporar[index]->leg;
cout << "\n" << "A fost sters: " << temporar[index]->data;
temporar[index]->data = -1;
temporar[index] = NULL;

37
free(temporar[index]);
}
int main(void){
int opt, n, cheie; char c;
Dictionar d;
do {//afisare meniu
cout << "\nMENIU:\n1.Creare";
cout << "\n2.Cautare valoare\n3.Stergere valoare";
cout << "\nOptiunea ta este:"; cin >> opt;
switch (opt){
case 1:
cout << "\nNumarul de elemente de inserat:";
cin >> n;
cout << "\nElementele de inserat sunt:";
for (int k = 0; k < n; k++){
cin >> cheie; d.inserare(cheie);}
break;
case 2:
cout << "\nSe va cauta elementul:"; cin >>
cheie;
d.cautare(cheie);
break;
case 3:
cout << "\nSe va sterge elementul:";
cin >> cheie;
d.stergere(cheie);
break;
default:
cout << "\nOptiune incorecta.";
break;
}
cout <<
"\nApasa D sau d pentru a continua. Altfel, programul
se opreste.";
cin >> c;
} while (c == 'd' || c == 'D');
}

3. Antrenamente C++
3.1. Noţiuni fundamentale

Recapitulează fundamentele programării orientate pe obiecte. Acum ştii ....

1. Ce este un obiect?

2. Care sunt principalele avantaje ale folosirii obiectelor?

38
3. Cum se creează un obiect?

4. Avantajele şi dezavantajele utilizării funcţiilor inline.

5. Care sunt restricţiile impuse funcţiilor inline?

6. Ce este moştenirea?

7. Ce este moştenirea multiplă?

8. Cum se folosesc două variabile cu acelaşi nume într-un program C++?

9. Ce sunt funcţiile constructor şi destructor?

10. Ce tip de dată poate returna un constructor?

11. Cum se apelează constructorii?

12. Ce tipuri de constructori sunt permişi în C++?

13. Ce sunt constructorii de copiere?

14. Cum se atribuie valoarea unui obiect, altui obiect?

15. Ce este o listă de iniţializare?

16. Cum se folosesc funcţiile constructor în cadrul moştenirii?

17. Cum acţionează funcţiile destructor în cadrul moştenirii?

18. Câţi destructori poate avea o clasă?

19. Ce este un membru protejat şi la ce foloseşte?

20. Variabilele membru private pot fi accesate şi în afara clasei respective?

21. Regulile privind accesul la membrii claselor în cazul derivării claselor.

22. Cum se accesează un membru al unui obiect accesat direct? Dar un membru al unui obiect
accesat indirect, prin intermediul unui pointer?

23. Ce înseamnă supraîncărcarea funcţiilor?

24. O clasă poate avea mai mulţi constructori? Dacă da, atunci cum ştie compilatorul să facă
diferenţierea între aceştia?

25. Ce înseamnă redefinirea metodelor în cadrul claselor specializate?

39
26. Ordinea claselor de bază, moştenite de o clasă, este importantă?

27. Ordinea de apel a constructorilor claselor derivate este importantă?

28. Cum se utilizeazaă parametrii prestabiliţi?

29. Cum se fac conversiile între clasa de bază şi clasa derivată?

30. Ce reprezintă supraîncărcarea operatorilor?

31. Cum se utilizează un operator supraîncărcat?

32. Care sunt operatorii C++ supraîncărcabili?

33. Ce operatori C++ nu se pot supraîncărca?

34. Cum se creează un operator supraîncărcat folosind o funcţie membru a clasei?

35. Cum se creează un operator supraîncărcat folosind o funcţie friend a clasei.

36. Cum se alocă memorie în programele C++?

37. Cum se eliberează memnoria alocată anterior?

38. Ce este polimorfismul şi la ce foloseşte?

39. Ce tipuri de polimorfism suportă limbajul C++?

40. Ce este o funcţie virtuală şi cum se defineşte?

41. Pot fi declaraţi constructorii ca fiind virtuali? Dar destructorii?

42. Ce este o funcţie virtuală pură?

43. Ce este o clasă abstractă?

44. Ce este o interfaţă în C++?

45. Ce este legarea internă şi externă?

46. Care este diferenţa dintre clase (class) şi structuri (struct)?

47. Ce trebuie specificat la deschiderea unui fişier?

48. Ce clase sunt definite în fişierul fstream.h?

49. Care sunt modurile de deschidere a fişierelor?

50. Cum se deschide un fişier?

40
51. Cum se închide un fişier?

52. Cum se realizează operaţiile de intrare / ieşire după deschiderea fişierului?

53. Cum poate fi testat rezultatul operaţiilor de intrare / ieşire?

54. Cum se poziţionează pointerul (indicatorul) de fişier pentru operaţiile de intrare / ieşire?

55. Ce sunt cazurile de excepţie?

56. Cum se realizează tratarea excepţiilor în C++?

57. Ce este un şablon şi la ce foloseşte?

58. Cum se creează un şablon de funcţie?

59. Cum se creează un şablon de clasă?

60. Cum se creează obiecte folosind un şablon de clasă?

3.2. Studii de caz

Fără a utiliza un compilator sau mediu de programare analizaţi următoarele secvenţe C++ (studii de
caz) şi rezolvaţi corect cerinţele de mai jos.

Studiul de caz nr. 1. Analizaţi codul următor (constructor, destructor, excepţii, obiecte). Care este
rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
int m_n;
A(int n): m_n(n){if(0 == n) {throw "Examen";}}
~A(){std::cout << m_n;}
};
int main(){
try {A a(1); A b(3); A c(2);} catch( ... ) {std::cout << 3;}
return 0;
}

Studiul de caz nr. 2. Analizaţi codul următor (constructor, şabloane, obiecte). Care este rezultatul
afişat pe ecran în urma executării programului?

#include <iostream>
template <unsigned N>
class A{
public: A(){std::cout << N;}
private: A<N-1> m_a;

41
};
template<> class A<0>{
public: A() {std::cout << 'A';}
};
int main(){A<4>(); return 0;}

Studiul de caz nr. 3. Analizaţi codul următor (clase, constructor, destructor, obiecte). Care este
rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
A(int n=2) : m_i(n){}
~A(){std::cout << m_i;}
protected: int m_i;
};
class B : public A{
public: B(int n): m_x(m_i+1), m_a(n){}
public: ~B(){std::cout << m_i; --m_i;}
private: A m_x; A m_a;
};
int main(){ B b(5); return 0;}

Studiul de caz nr. 4. Analizaţi codul următor (clase constructori , c#ampuri statice, obiecte). Care este
rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A {
public: A(int n=0) : m_i(n){ std::cout << m_i;}
protected: int m_i;
};
class B : public A {
public: B(int n):m_j(n), m_a(--m_j), m_b(){std::cout << m_j;}
private: int m_j; A m_a; A m_b;
static A m_c;
};
A B::m_c(3);
int main(){B b(2); return 0;}

Studiul de caz nr. 5. Analizaţi codul următor (clase, constructori , operatorii new şi delete, obiecte).
Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public: A(int n=0):m_i(n){std::cout << m_i; ++m_i;}
protected: int m_i;
};
class B: public A{
public: B(int n=5):m_a(new A[2]), m_x(++m_i){std::cout << m_i;}

42
~B(){delete [] m_a;}
private: A m_x;
A* m_a;
};
int main(){B b; return 0;}

Studiul de caz nr. 6. Analizaţi codul următor (clase, constructori, destructori, excepţii, obiecte). Care
este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public: A(int n):m_n(n){std::cout << m_n;}
~A() { std::cout << m_n;}
private: int m_n;
};
int f(int n){ if (1==n) {throw 0;} A l(n); return f(n-1)*n/(n-
1); }
int main(){
try {int r = f(3); A a(r);}
catch (int e){std::cout << e << std::endl;}
return 0;
}

Studiul de caz nr. 7. Analizaţi codul următor (clase, constructori, destructori, gestiunea dinamică a
obiectelor, virtualizare). Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
virtual ~A(){f();}
public:
virtual void f() const{ std::cout << 1;}
};
class B: public A {
public:
~B(){f();}
private:
virtual void f() const {std::cout << 2;}
};
int main(){
A* a = new B;
delete a;
std::cout << std::endl;
return 0;
}

Studiul de caz nr. 8. Analizaţi codul următor (clase, constructori, destructori, gestiunea dinamică a
obiectelor, exceptii). Care este rezultatul afişat pe ecran în urma executării programului?

43
#include <iostream>
class A {
public:
A(int n){if (0==n){throw "Examen";}}
};
int main(){
A *p0 = 0, *p1 = 0, *p2 = 0;
try{
p1 = new A(1);
p0 = new A(0);
p2 = new A(2);
} catch( ... ){std::cout << 3;}// orice exceptie
std::cout << (( 0 != p1)? 1:0);
std::cout << (( 0 != p0)? 1:0);
std::cout << (( 0 != p2)? 1:0) << std::endl;
delete p1;
delete p0;
delete p2;
return 0;
}

Studiul de caz nr. 9. Analizaţi codul următor (clase, constructori, destructori, gestiunea dinamică a
obiectelor, exceptii). Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class C{
public:
C(int i): i(i){std::cout << i;}
~C(){std::cout << i+5;}
private: int i;
};
int main(){
C(2);
const C& c = C(1); return 0;
}

Studiul de caz nr. 10. Analizaţi codul următor (clase, constructori, destructori, referinţe, virtualizare).
Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class B{
public:
virtual int shift(int n = 2) const { return n << 2;}
};
class D: public B {
public:
int shift(int n = 3) const{ return n <<3;}
};
int main(){

44
const D d;
const B* b = &d;
std::cout << b -> shift(); return 0;
}

Studiul de caz nr. 11. Analizaţi codul următor (clase, constructori, destructori, referinţe, virtualizare).
Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
virtual void f(int n){std::cout << n << 1;}
virtual ~A(){}
void f(int n) const { std::cout << n;}
};
class B: public A{
public: void f(int n){std::cout << (n << 1);}
void f(int n) const {std::cout << n+1;}
};
int main(){
const A a;
B b;
A& c = b;
const A* d = &b;
a.f(2);
b.f(2);
c.f(1);
d->f(1);
return 0;
}

Studiul de caz nr. 12. Analizaţi codul următor (clase, constructori, destructori, alocare / eliberare,
virtualizare). Care este rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
A(){f();}
virtual ~A(){}
public:
virtual void f() const{ std::cout << 1;}
};
class B: public A {
public:
B(){f();}
private:
virtual void f() const {std::cout << 2;}
};
int main(){

45
A* a = new B;
delete a;
std::cout << std::endl;
return 0;
}

Studiul de caz nr. 13. Analizaţi codul următor (cu ierarhie de clase). Care este rezultatul afişat pe
ecran în urma executării programului?

#include <iostream>
class E{
typedef int INT;
};
class D : public E{};
class C : public D{};

int main(){
std :: cout << sizeof(E) << " " << sizeof(D) << " " <<
sizeof(C) <<std :: endl;
return 0;
}

Studiul de caz nr. 14. Analizaţi codul următor (cu ierarhie de clase şi virtualizare). Care este
rezultatul afişat pe ecran în urma executării programului?

class B{
public:
B(){P();}
~B(){P();}
void Q() {P();}
virtual void P(int x = 0x10){ std::cout << "U::P:x = " <<
x << std::endl;}
};
class C : public B {
public:
C(){}
virtual void P(int x = 0x100) { std::cout << "V::P:x = "
<< x << std::endl;}
};
int main(){
B *b = new C();
b -> Q();
delete b;
return 0;
}

Studiul de caz nr. 15. Analizaţi codul următor (cu clase şi virtualizare). Care este rezultatul afişat pe
ecran în urma executării programului?

46
#include <iostream>
class A{
public:
~A(){std :: cout << "A :: ~A()" << std :: endl;}
};
class B{
public:
~B(){std :: cout << "B :: ~B()" << std :: endl;}
};
class S{
A a;
B b;
public:
S(): b(), a() {}
virtual ~S() {std :: cout << "S :: ~S()" << std :: endl;}
};
int main (void) {S s; return 0;}

Studiul de caz nr. 16. Analizaţi codul următor (cu moştenire multiplă şi virtualizare). Care este
rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
~A(){std :: cout << "A :: ~A()" << std :: endl;}
};
class B{
public:
~B(){std :: cout << "B :: ~B()" << std :: endl;}
};
class S : public A, public B {
public:
virtual ~S() {std :: cout << "S :: ~S()" << std :: endl;}
};
int main (void) {S* s = new S(); delete s; return 0;}

Studiul de caz nr. 17. Analizaţi codul următor (cu ierarhie de clase şi virtualizare). Care este
rezultatul afişat pe ecran în urma executării programului?

#include <iostream>
class F{
public:
F() {std :: cout << "F::F()" << std :: endl;}
virtual ~F(){std :: cout << "F::~F()" << std :: endl;}
};
class A: public F{
int x, y;
public:
A(){std :: cout << "A::A()" << std :: endl;}

47
virtual ~A(){std :: cout << "A::~A()" << std :: endl;}
};
class B: public A{
int z;
public:
B(int z = 0) {std :: cout << "B::B()" << std :: endl;}
virtual ~B(){std :: cout << "B::~B()" << std :: endl;}
};
int main (void) {
F * f[10]; int n = 0;
f[n++] = new B();
f[n++] = new A();
for (int i=0; i<n; i++) delete f[i];
return 0;
}

Studiul de caz nr. 18. Analizaţi codul următor (tipuri de constructori). Care este rezultatul afişat pe
ecran în urma executării programului?

#include <iostream>
class A{
public:
A(){ std :: cout <<"A::A()" << std :: endl;}
A(A &a){std :: cout <<"A::A(A&)" << std :: endl;}
~A(){std :: cout <<"A::~A()" << std :: endl;}
};
int main (void) {
A a;
A x = a;
A y(x);
return 0;
}

Studiul de caz nr. 19. Analizaţi codul următor (supraîncărcare operatori ++ şi --). Care este rezultatul
afişat pe ecran în urma executării programului?

#include <iostream>
class A{
public:
A(float pa): a(pa){}
float Get(){return a;}
A operator++(){a++; return *this;}
A operator--(){a--; return *this;}
private:
float a;
};
int main (void) {
A a(1.0);
std :: cout << (++ (++ a)).Get() << std :: endl;

48
std :: cout <<a.Get() << std :: endl;
std :: cout << (-- (-- a)).Get() << std :: endl;
std :: cout <<a.Get() << std :: endl;
return 0;
}

Studiul de caz nr. 20. Analizaţi codul următor (lucru cu vectori STL). Simulaţi executarea
programului pentru primii 10 termeni ai şirului lui Fibonacci: 0,1, 1, 2, 3, 5, 8, 13, 21, 34.

#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
using namespace std;
int main(){
vector<int> v;
vector<int>::iterator k;
int opt, x;
while (1){
cout<<"\n---------------------"<<endl;
cout<<"Vector STL"<<endl;
cout<<"\n---------------------"<<endl;
cout<<"1.Insert(e)"<<endl;
cout<<"2.Delete(e)"<<endl;
cout<<"3.Dim(v)"<<endl;
cout<<"4.Display(v)"<<endl;
cout<<"5.Iterate(v)"<<endl;
cout<<"6.Clear(v)"<<endl;
cout<<"7.Exit"<<endl;
cout<<"Alege =: ";
cin>>opt;
switch(opt) {
case 1:
cout<<"Val = "; cin>>x; v.push_back(x); break;
case 2:
cout<<"Del:"<<endl; v.pop_back(); break;
case 3:
cout<<"Dim: "; cout<<v.size()<<endl; break;
case 4:
cout<<"Display(v): ";
for (int index = 0; index < v.size(); index++){
cout<<v[index]<<" ";}
cout<<endl;
break;
case 5:
cout<<"Iterare: ";
for (k = v.begin(); k != v.end(); k++){ cout<<*k<<"
"; }
cout<<endl;

49
break;
case 6:
v.clear(); cout<<"Curat!"<<endl; break;
case 7:
exit(1); break;
default:
cout<<"Alegere gresita"<<endl;
}
}
return 0;
}

3.3. Probleme propuse spre rezolvare

Folosiţi noţiunile de bază POO şi cunoştinţe fundamentale de algoritmică şi programare, rezolvaţi


problemele de mai jos.

Problema 1 [Bară Orizontală] Definiţi o clasă BaraO al cărei constructor tipăreşte la consolă, pe un
r#and nou, +-------+ (şirul începe cu semnul „plus”, apoi sunt semne „minus”, iar ultimul semn
din şir este un „plus”). Numărul de semne minus se transmite constructorului ca argument.

Problema 2 [Bară verticală] Definiţi o clasă BarV al cărei constructor tipăreşte un anumit număr de
semne „|” în felul următor:
|
|
|
Numărul de semne “| “ se dă ca argument constructorului.

Problema 3 [Numere raţionale] Creaţi un tip de date definit de utilizator(o clasă) pentru numere
raţionale. Numele tipului va fi Fractie. Definiţi pentru acest tip de date operaţiile de adunare, scădere,
înmulţire, împărţire, inversare, egalitate

Problema 4 [Timpul-1] Creaţi un tip de date definit de utilizator (Timp) pentru reprezentarea timpului
sub forma (ora:minutul:secunda.sutimea). Pentru acest tip de date implemetaţi operaţia de adunare a
doi timpi. Se vor utiliza doi constructori ai obiectelor de tip Timp ( două modalităţi de iniţializare a
unui obiect de tip Timp):
Timp t1(3, 15, 01, 02);
Timp t2(“3.15.01.02”);
Implementaţi o metodă care să determine timpul scurs de la o anumită ora până la ora curentă. “Ora
curentă” este cea dată de ceasul intern al calculatorului.

Problema 5 [Timpul-2] Creaţi un tip de date definit de utilizator (Timp) pentru reprezentarea timpului
sub forma (ora:minutul:secunda.sutimea). Pentru acest tip de date implementaţi o metodă care să
determine timpul scurs de la o anumită ora până la ora curentă. “Ora curentă” este cea dată de ceasul
intern al calculatorului. Se vor utiliza doi constructori ai obiectelor de tip Timp ( două modalităţi de
iniţializare a unui obiect de tip Timp):
Timp t1(3, 15, 01, 02);
Timp t2(“3.15.01.02”);

50
Problema 6 [Ziua de azi] Creaţi un tip de date definit de utilizator (Data) pentru reprezentarea datei
sub forma (zi.luna.an). Pentru acest tip de date implementaţi o operaţie pentru determinarea numărului
de zile dintre două date. Se vor utiliza două modalităţi (constructori) de iniţializare a unui obiect de tip
Data:
Date d1(25, 1, 1961);
Date d2(“2.92.1992”);

Problema 7 [Vârsta unei persoane] Creaţi un tip de date definit de utilizator (Data) pentru
reprezentarea datei sub forma (zi.luna.an). Se vor utiliza două modalităţi (constructori) de iniţializare a
unui obiect de tip Data:
Date d1(25, 1, 1961);
Date d2(“8.6.1884”);
Pentru acest tip de date implementaţi o metodă care să determine vârsta unei persoane în ani, luni şi
zile. Data curentă va fi cea dată de ceasul intern al sistemului de calcul.

Problema 8 [Vectori în plan] Un vector în plan se poate specifica printr-o pereche de numere reale. Să
se implementeaze o clasa Vector2 pentru lucrul cu vectori în plan care să suporte următorii operatori
(supraîncărcaţi): adunare, scădere, înmulţire cu un scalar şi produsul scalar a doi vectori. Să se scrie un
program demonstrativ care utilizează clasa Vector2.

Problema 9 [Matrice] Sa se scrie un program care implementează clasa Matrice şi suportă operaţiile:
creare, afişare, adunare, diferenţă, transpunere. Programul demonstrativ citeşte de la tastatură două
matrice, calculeaza suma, diferenta şi transpusa acestora, cu afişarea rezultatului obţinut după fiecare
operaţie executată.

Problema 10 [Greedy - Conexitate] Pentru o clasa Graf se cere sa implementaţi o metodă pentru
determinarea componentei conexe a unui nod dat.

Problema 11 [Backtracking - Dame] Se cere un program C++ pentru rezolvarea problemei celor n
dame, n>3.

Problema 12 [Backtracking - Permutări] Se cere un program C++ pentru generarea tuturor


permutărilor unei mulţimi cu n elemente, n>1.

Problema 13 [Backtracking – Submulţimi] Se cere să se determine toate submulţimile cu m elemente


ale unei mulţimi cu n elemente, unde 0 < m <= n.

Problema 14 [Greedy – Arbore parţial de cost minim] Se cere un program C++ cu clase pentru
determinarea arborelui parţial de cost minim al unui graf neorientat ponderat.

Problema 15 [Programare dinamică – Distanţe minime] Se cere un program C++ cu clase pentru
determinarea matricei distanţelor minime dintre oricare două noduri.

Problema 16 [Divide et impera – Sortare prin interclasare] Se cere implementarea unei clase tablou de
numere reale pentru care să poată fi aplicată sortarea prin interclasare (merge sort).

Problema 17 [Divide et impera – Sortare rapidă ] Se cere implementarea unei clase tablou de numere
reale pentru care să poată fi aplicată metoda sortării rapide (Quick sort).

51
Problema 18 [Căutare binară] Se cere implementarea unei clase tablou de numere reale pentru care să
poată fi aplicată metoda căutării binare.

Problema 19 [Sortare prin metoda bulelor] Se cere implementarea unei clase tablou de numere reale
pentru care să poată fi sortat prin metoda bulelor (bubble sort).

Problema 20 [Sortare prin metoda selecţiei] Se cere implementarea unei clase tablou de numere reale
pentru care să poată fi sortat prin metoda selecţiei.

Problema 21 [Sortare prin metoda inserţiei] Se cere implementarea unei clase tablou de numere reale
pentru care să poată fi sortat prin metoda inserţiei.

Problema 22 [Explorarea arborilor în lătime] Se cere implementarea unei clase pentru stocarea
arborilor oarecare care să suporte explorarea în lăţime a obiectelor de tip arbore oarecare.

Problema 23 [Explorarea în adâncime a arborilor binari] Se cere implementarea unei clase pentru
stocarea arborilor binari care să suporte explorarea în inordine, preordine şi postordine a obiectelor de
tip arbore binar.

Problema 24 [Rutare în grafuri – Algoritmul lui Dijkstra] Se cere un program C++ cu clase pentru
stocarea unui graf (orientat) ponderat şi să permită determinarea rutelor minime de la un nod la oricare
din celelalte noduri.

52
Bibliografie
1. Alexandrescu, A., Programare modernă în C++, Teora, 2002.
2. Andonie, R., Gârbacea, I., Algoritmi fundamentali. O perspectiva C++, Editura Libris, 1995.
3. Bălănescu, T., Proiectare şi programare orientată pe obiecte, Sinteze, Bibliteca virtuală,
Universitatea Spiru Haret, 2007.
4. Berian, D., Cocoş, A., Programare orientată pe obiecte, UPT, Îndrumător de laborator, 2008.
5. Gălăţan, C., Introducere în Standard Template Library, Editura All, 2008.
6. Jamsa K., Klander L., Totul despre C şi C++ - Manualul fundamental de programare în C şi
C++, Teora, 1999-2006.
7. Pătruţ, B., Muraru, C.V., Aplicaţii în C şi C++, EduSoft, 2006.
8. Schildt H., C++, manual complet. Teora, 1997.
9. ***, C++ Programming Examples on Graph Problems & Algorithms,
http://www.sanfoundry.com/cpp-programming-examples-graph-problems-algorithms/
10. ***, Informatică pentru examenul de licenţă. Universitatea Babeş Bolyai, 2016,
http://www.cs.ubbcluj.ro/wp-content/uploads/Manual_Informatica_2016_RO.pdf

53
II. Baze de date
Nicoleta Magdalena Iacob, Conf. univ. dr., Universitatea Spiru Haret

1. Noţiuni introductive în teoria bazelor de date


1.1. Noțiunile de bază de date, sistem de gestiune a bazei de date

O bază de date (BD):


• reprezintă un ansamblu structurat de fişiere care grupează datele prelucrate în aplicaţii
informatice ale unei persoane, grup de persoane, instituţii etc.;
• este definită ca o colecţie de date aflate în interdependenţă, împreună cu descrierea datelor şi a
relaţiilor dintre ele.

Sistemul de gestiune a bazei de date (SGBD) este:


• un sistem complex de programe care asigură interfaţa între o bază de date şi utilizatorii
acesteia;
• software-ul bazei de date care asigură:
o definirea structurii bazei de date;
o încărcarea datelor în baza de date;
o accesul la baza de date (interogare, actualizare);
o întreţinerea bazei de date (refolosirea spaţiilor goale, refacerea bazei de date în cazul
unor incidente);
o reorganizarea bazei de date (restructurarea şi modificarea strategiei de acces);
o securitatea datelor.

1.2. Noțiunile de entitate, relație, atribut

Cele trei concepte de bază utilizate în organizarea bazei de date sunt:


• entitatea;
• atributul;
• valoarea.

Prin entitate se înţelege un obiect concret sau abstract reprezentat prin proprietăţile sale. O proprietate
a unui obiect poate fi exprimată prin perechea (ATRIBUT, VALOARE).

Entitatea poate fi persoană, loc, concept, activitate etc. Prin urmare, ea poate fi un obiect cu existenţă
fizică, reală sau poate fi un obiect cu existenţă conceptuală, abstractă.

Exemplu: În exemplul “Produsul X are culoarea Y”, atributul este „culoarea”, iar valoarea este
reprezentată litera„Y”.

O entitate poate fi:


• dependentă (slabă), existenţa sa depinzând de altă entitate;
• independentă (tare), caz în care ea nu depinde de existenţa altei entităţi.

Observaţii privind entitățile:


• entităţile devin tabele în modelele relaţionale;
• în general, entităţile se scriu cu litere mari;
• entităţile sunt substantive, dar nu orice substantiv este o entitate. Trebuie ignorate
substantivele nerelevante;
54
• cheia primară identifică unic o entitate şi face distincţie între valori diferite ale entităţii.
Aceasta trebuie să fie unică şi cunoscută la orice moment. Cheia primară trebuie să fie
controlată de administratorul bazei, să nu conţină informaţii descriptive, să fie simplă, fără
ambiguităţi, să fie stabilă, să fie familiară utilizatorului astfel încât acesta să o poată folosi
cu uşurinţă;
• pentru fiecare entitate este obligatoriu să se dea o descriere detaliată;
• nu pot exista, în aceeaşi diagramă, două entităţi cu acelaşi nume, sau o aceeaşi entitate cu
nume diferite.

Relaţia este o comunicare între două sau mai multe entităţi. Gradul unei relaţii este dat de numărul de
entităţi participante într-o relaţie (de exemplu, relaţie binară, ternară, cvadruplă, n-ară).
Existenţa unei relaţii este subordonată existenţei entităţilor pe care le leagă.

O relaţie în care aceeaşi entitate participă mai mult decât o dată în diferite roluri defineşte o relaţie
recursivă. Uneori, aceste relaţii sunt numite unare.

Observaţii privind relațiile:


• în modelul relaţional, relaţiile devin tabele speciale sau coloane speciale care referă chei
primare;
• relaţiile sunt verbe, dar nu orice verb este o relaţie;
• pentru fiecare relaţie este important să se dea o descriere detaliată;
• într-o relaţie, tuplurile trebuie să fie distincte;
• în aceeaşi diagramă pot exista relaţii diferite cu acelaşi nume. În acest caz, ele sunt diferenţiate
de către entităţile care sunt asociate prin relaţia respectivă;
• cardinalul relaţiei este numărul tuplurilor dintr-o relaţie. Pentru fiecare relaţie trebuie stabilită
cardinalitatea (maximă şi minimă) relaţiei.

Asupra entităţilor participante într-o relaţie pot fi impuse constrângeri care trebuie să reflecte
restricţiile care există în lumea reală asupra relaţiilor. O clasă de constrângeri, numite constrângeri de
cardinalitate, este definită de numărul de înregistrări posibile pentru fiecare entitate participantă (raport
de cardinalitate). Cel mai întâlnit tip de relaţii este cel binar, iar în acest caz rapoartele de cardinalitate
sunt, în general, one-to-one (1:1), one-to-many (1:M) sau many-to-many (N:M).

• 1:1 - legătura de tip “una-la-una” (one-to-one) - este asocierea prin care unui element
(entitate) din mulţimea E1 îi corespunde un singur element din mulţimea E2 şi reciproc;
• 1:M – legătura de tip “una-la-multe” (one-to-many) - este asocierea prin care unui element
din mulţimea E1 îi corespund unul sau mai multe elemente din mulţimea E2, dar unui
element din E2 îi corespunde un singur element în mulţimea E1;
• N:M - legătura de tip “multe-la-multe” (many-to-many) - este asocierea prin care unui
element din mulţimea E1 îi corespund unul sau mai multe elemente din mulţimea E2 şi
reciproc.
O relaţie se poate memora printr-un tabel de forma:

R A1 ... Ai .. An
r1 a11 ... a1j ... a1n
... ... ... ... ... ...
ri ai1 ... aij ... ain
... ... ... ... ... ...
rm am1 ... amj ... amn

55
unde liniile din acest tabel formează elementele relaţiei, sau tupluri, înregistrări, care în general sunt
distincte, coloanele A1, A2, ..., An formeaza o mulţime de atribute, iar a11, …, amn sunt valoari pentru
fiecare din atributele A1, An.

Atributul este o proprietate descriptivă a unei entităţi sau a unei relaţii (de exemplu, denumire,
unitate_de_masura a unui produs, sunt atribute al entităţii PRODUS).

Atributele pot fi:


• simple (de exemplu, pretul unitar al unui produs);
• compuse (de exemplu, denumirea produsului);
• cu valori multiple (de exemplu, limbile în care e tradus un produs);
• derivate (de exemplu, numarul de zile rămase pănă la expirare se obţine din data expirării).

Atributele sunt utile atunci când într-o relaţie un domeniu apare de mai multe ori. Prin numele dat
fiecărei coloane (atribut), se diferenţiază coloanele care conţin valori ale aceluiaşi domeniu, eliminând
dependenţa faţă de ordine.

Observaţii privind atributele:


• atributul reprezintă coloana unei tabele de date, caracterizată printr-un nume;
• trebuie făcută distincţie între atribut, care uzual devine coloană în modelele relaţionale şi
valoarea acestuia, care devine valoare în coloane;
• atributele sunt substantive, dar nu orice substantiv este atribut;
• fiecărui atribut trebuie să i se dea o descriere completă în specificaţiile modelului (exemple,
contraexemple, caracteristici);
• pentru fiecare atribut trebuie specificat numele, tipul fizic (integer, float, char etc.), valori
posibile, valori implicite, reguli de validare, constrângeri, tipuri compuse;
• atributele pot caracteriza o clasă de entităţi, nu doar o entitate.

1.3. Construirea de diagrame entitate-relaţie

Prin tehnica entiate-relaţie (denumită şi entitate-asociere) se construieşte o diagramă entiate-relaţie


(notată E-R) prin parcurgerea următorilor paşi:
a) identificarea entităţilor (componentelor) din sistemul proiectului;
b) identificarea asocierilor (relaţiilor) dintre entităţi şi calificarea lor;
c) identificarea atributelor corespunzătoare entităţilor;
d) stabilirea atributelor de identificare a entităţilor.

a) Identificarea entităţilor. Prin convenţie, entităţile sunt substantive, se scriu cu litere mari şi se
reprezintă prin dreptunghiuri. Într-o diagramă nu pot exista două entităţi cu acelaşi nume, sau o
aceeaşi entitate cu nume diferite.

b) Identificarea asocierilor dintre entităţi şi calificarea lor. Între majoritatea componentelor (adică a
entităţilor) unui sistem economic se stabilesc legături (asocieri).

Observaţii privind diagramele entiate-relaţie:


• legăturile se reprezintă prin arce neorientate între entităţi;
• fiecărei legături i se acordă un nume plasat la mijlocul arcului şi simbolizat printr-un romb
(semnificaţia legăturii);
• numerele simbolizate deasupra arcelor se numesc cardinalităţi şi reprezintă tipul legăturii;

56
• cardinalitatea asocierilor exprimă numărul minim şi maxim de realizări ale unei entităţi cu
cealaltă entitate asociată. Maximele unei cardinalităţi sunt cunoscute şi sub denumirea de
grad de asociere, iar minimele unei cardinalităţi reprezintă obligativitatea participării
entităţilor la asociere.

1.4. Tipuri de relații între entităţi

Asocierea dintre entităţi se face în funcţie de:


• cardinalitatea asocierii. În funcţie de maxima cardinalităţii (gradul de asociere), se cunosc trei
tipuri de asocieri:
o una-la-una (1-1 sau one to one);
o una-la-multe (1-M sau one to many);
o multe-la-multe (N-M sau many to many).
• numărul de entităţi distincte care participă la asociere. Se cunosc trei tipuri de asocieri:
o binare (între două entităţi distincte);
o recursive (asocieri ale entităţilor cu ele însele);
o complexe (între mai mult de două entităţi distincte).

c) Identificarea atributelor entităţilor şi a asocierilor dintre entităţi. Atributele unei entităţi reprezintă
proprietăţi ale acestora. Atributele sunt substantive, iar pentru fiecare atribut i se va preciza tipul fizic
(integer, float, char, string etc.)

d) Stabilirea atributelor de identificare a entităţilor. Un atribut de identificare (numit cheie primară),


reprezintă un atribut care se caracterizează prin unicitatea valorii sale pentru fiecare instanţă a entităţii.

În cadrul diagramei entitate-asociere, un atribut de identificare se marchează prin subliniere sau prin
marcarea cu simbolul # plasat la sfârşitul numelui acestuia.

Pentru ca un atribut să fie atribut de identificare, acesta trebuie să satisfacă unele cerinţe:
• oferă o identificare unică în cadrul entităţii;
• este uşor de utilizat;
• este scurt (de cele mai multe ori, atributul de identificare apare şi în alte entităţi, drept cheie
externă).

Pentru o entitate pot exista mai multe atribute de identificare, numite atribute (chei) candidate. Dacă
există mai mulţi candidaţi cheie, se va selecta unul, preferându-se acela cu valori mai scurte şi mai
puţin volatile.

2. Baze de date relaţionale


2.1. Noțiunile de bază de date relaţională, sistem de gestiune a bazelor de date relaţionale

O bază de date relaţională (BDR) reprezintă un ansamblu de relaţii, prin care se reprezintă datele şi
legăturile dintre ele.

În cadrul bazei de date relaţionale, datele sunt organizate sub forma unor tablouri bidimensionale
(tabele) de date, numite relaţii. Asocierile dintre relaţii se reprezintă prin atributele de legătură. În
cazul legăturilor de tip „una-la-multe”, aceste atribute figurează într-una dintre relaţiile implicate în
asociere. În cazul legăturilor de tip „multe-la-multe”, atributele sunt situate într-o relaţie distinctă,
construită special pentru explicarea legăturilor între relaţii.

57
Prin sistem de gestiune a bazelor de date relaţionale (SGBDR) se înţelege un SGBD care utilizează
drept concepţie de organizare a datelor modelul relaţional.

Definirea unui SGBDR impune o detaliere a caracteristicilor pe care trebuie să le prezinte un SGBD
pentru a putea fi considerat relaţional. În acest sens, Codd a formulat (în 1985) 13 reguli, care exprimă
cerinţele pe care trebuie să le satisfacă un SGBD.

2.2. Regulile lui Codd

Regulile lui Codd pentru SGBD-urile relaţionale sunt:


R0: Gestionarea datelor la nivel de relaţie. Sistemul trebuie să gestioneze BD numai prin mecanisme
relaţionale.

R1: Reprezentarea logică a datelor. Într-o bază de date relaţionată, informaţia este reprezentată la nivel
logic sub forma unor tabele (relaţii). Acest lucru înseamnă că toate datele trebuie să fie memorate şi
prelucrate în acelaşi mod.

R2: Garantarea accesului la date. Orice dată din baza de date relaţionată trebuie să poată fi accesată
prin specificarea: numelui relaţiei (tabelei), a valorii cheii primare și numelui atributului (coloană).

R3: Valorile nule:


• sistemul trebuie să permită declararea şi manipularea sistematică a valorilor NULL (semnifică
lipsa unor date);
• valorile NULL diferă de şirurile de caractere „spaţiu”, şirurile vide de caractere.
• valorile NULL sunt deosebit de importante în implementarea restricţiilor de integritate:
integritatea entităţilor; integritatea referenţială.

R4: Metadatele. Utilizatorii autorizaţi trebuie să poată aplica asupra descrierii bazei de date aceleaşi
operaţii ca şi asupra datelor obişnuite.

R5: Facilităţile limbajelor utilizate:


• trebuie să existe cel puţin un limbaj care să exprime oricare din următoarele operaţii:
o definirea relaţiilor;
o să vizualizeze datele;
o să regăsească informaţia;
o să poată reactualiza informaţia;
o să verifice şi să corecteze datele de intrare etc.
• în general, toate implementările SQL respectă această regulă.

R6: Actualizarea tabelelor virtuale:


• toate tabelele/relaţiile virtuale trebuie să poată fi actualizate;
• nu toate tabelele virtuale sunt teoretic actualizate.

Exemplu: Fie tabela de bază PRODUS, cu următoarea schemă PRODUS (Denumire_produs:D1,


Cantitate:D2, Pret_unitar:D3), cu ajutorul tabelei PRODUS este definită o tabelă virtuală FACTURA,
cu schema: FACTURA (Denumire_produs:D1, Cantitate:D2, Pret_unitar:D3, Valoare:D4). Valorile
atributului „Valoare” se calculează astfel:
Valoare=Cantitate*Pret_unitar

58
Presupunem că se doreşte schimbarea preţului unitar la un anumit produs, această schimbare trebuie
efectuată în tabela de bază PRODUS, atributul „Pret_unitar” din tabela virtuală FACTURA, fiind
actualizabil, întrucât actualizarea se poate propaga spre tabela de bază.

Presupunem că se doreşte schimbarea valorii „Valoare” la un anumit produs:


 modificarea de la tabela virtuală spre tabela de bază nu mai este posibilă, atributul „Valoare”
nu este actualizabil, deoarece schimbarea valorii „Valoare” se poate datora schimbării
cantităţii „Cantitate” şi/sau a preţului unitar „Pret_unirar”;
 astfel trebuie să existe un mecanism prin care să se poată determina dacă anumite vizualizări
pot fi modificate sau nu.

Majoritatea implementărilor SQL îndeplinesc această cerinţă.

R7: Actualizările în baza de date (inserările, modificările şi ştergerile din baza de date):
• un SGBDR nu trebuie să oblige utilizatorul să caute într-o relaţie, tuplu cu tuplu, pentru a
regăsi informaţia dorită;
• această regulă exprimă cerinţa ca în operaţiile prin care se schimbă conţinutul bazei de date să
se lucreze la un moment dat pe o întreagă relaţie.

R8: Independenţa fizică a datelor:


• o schimbare a structurii fizice a datelor nu trebuie să blocheze funcţionarea programelor de
aplicaţii;
• într-un SGBDR trebuie să se separe aspectul fizic al datelor (stocare sau acces la date) de
aspectul logic al datelor.

R9: Independenţa logică a datelor. Schimbarea relaţiilor bazei de date nu trebuie să afecteze
programele de aplicaţie.

R10: Restricţiile de integritate. Restricţiile de integritate trebuie să fie definite într-un limbaj
relaţional, nu în programul de aplicaţie.

R11: Distribuirea geografică a datelor. Distribuirea datelor pe mai multe calculatoare dintr-o reţea de
comunicaţii de date, nu trebuie să afecteze programele de aplicaţie.

R12: Prelucrarea datelor la nivelul de bază. Dacă sistemul posedă un limbaj de bază orientat pe
prelucrarea de tupluri şi nu pe prelucrarea relaţiilor, acest limbaj nu trebuie să fie utilizat pentru a evita
restricţiile de integritate (se introduc inconsistenţe).

Clasificarea regulilor lui Codd


În funcţie de tipul de cerinţe pe care le exprimă, regulile sunt grupate în 5 categorii:
1) Reguli de bază: R0 şi R12;
2) Reguli structurale: R1 şi R6;
3) Reguli privind integritatea datelor: R3 şi R10;
4) Reguli privind manipularea datelor: R2, R4, R5, R7;
5) Reguli privind independenţa datelor: R8, R9, R11.

2.3. Componentele bazelor de date relaționale:

Orice model de date, conform unei sugestii a lui Codd, trebuie să se bazeze pe trei componente:
1) structurile de date. O bază de date relaţională (BDR) reprezintă un ansamblu de relaţii, prin
care se reprezintă date şi legăturile dintre ele. Structurile sunt definite de un limbaj de definire
59
a datelor (Data Definition Language). Datele în modelul relațional sunt structurate în relații
(tabele) bidimensionale;
2) operatorii de manipulare a datelor. Relațiile pot fi manipulate utilizând un limbaj de
manipularea datelor (Data Manipulation Language). În modelul relațional, limbajul folosește
operatori relaționali bazați pe conceptul algebrei relaționale. În afară de acesta, există limbaje
echivalente algebrei relaționale, cum ar fi calculul relațional orientat pe tuplu și calculul
relațional orientat pe domeniu;
3) constrângerile de integritate. Prin integritatea datelor se subînțelege că datele rămân stabile, în
siguranță și corecte. Integritatea în modelul relațional este menținută de constrângeri interne
care nu sunt cunoscute utilizatorului.

1) Structura relaţională a datelor


Prezentarea structurii relaţionale a datelor impune definirea noţiunilor de: domeniu, tabelă (relaţie),
atribut, tuplu, cheie şi schema tabelei.

Domeniu este un ansamblu de valori caracterizat printr-un nume. El poate fi explicit sau implicit.

Tabela/relaţia este un subansamblu al produsului cartezian al mai multor domenii, caracterizat printr-
un nume, prin care se definesc atributele ce aparţin aceleaşi clase de entităţi.

Atributul este coloana unei tabele, caracterizată printr-un nume.

Cheia este un atribut sau un ansamblu de atribute care au rolul de a identifica un tuplu dintr-o tabelă.
Tipuri de chei: primare/alternate, simple/comune, externe.

Tuplul este linia dintr-o tabelă şi nu are nume. Ordinea liniilor (tuplurilor) şi coloanelor (atributelor)
dintr-o tabelă nu trebuie să prezinte nici-o importanţă.

Schema tabelei este formată din numele tabelei, urmat între paranteze rotunde de lista atributelor, iar
pentru fiecare atribut se precizează domeniul asociat.

Schema bazei de date poate fi reprezentată printr-o diagramă de structură în care sunt puse în evidenţă
şi legăturile dintre tabele. Definirea legăturilor dintre tabele se face logic construind asocieri între
tabele cu ajutorul unor atribute de legătură. Atributele implicate în realizarea legăturilor se găsesc fie
în tabelele asociate, fie în tabele distincte construite special pentru legături. Atributul din tabela iniţială
se numeşte cheie externă, iar cel din tabela finală este cheie primară. Legăturile posibile sunt 1:1,
1:M, M:N. Potenţial, orice tabelă se poate lega cu orice tabelă, după orice atribute.

Legăturile se stabilesc la momentul descrierii datelor prin limbaje de descriere a datelor (LDD), cu
ajutorul restricţiilor de integritate. Practic, se stabilesc şi legături dinamice la momentul execuţiei.

2) Operatorii modelului relaţional


Operatorii modelului relaţional sunt operatorii din:
a) algebra relaţională;
b) calcul relaţional: orientat pe tuplu; orientat pe domeniu

a) Algebra relaţională este o colecţie de operaţii formale aplicate asupra tabelelor (relaţiilor), şi a
fost concepută de E.F. Codd. Operaţiile sunt aplicate în expresiile algebrice relaţionale care sunt
cereri de regăsire. Acestea sunt compuse din operatorii relaţionali şi operanzi. Operanzii sunt
întotdeauna tabele (una sau mai multe). Rezultatul evaluării unei expresii relaţionale este format
dintr-o singură tabelă.
60
Algebra relaţională are cel puţin puterea de regăsire a calcului relaţional. O expresie din calculul
relaţional se poate transforma într-una echivalentă din algebra relaţională şi invers.

Codd a introdus 6 operatori de bază (reuniunea, diferenţa, produsul cartezian, selecţia, proiecţia,
joncţiunea) şi 2 operatori derivaţi (intersecţia şi diviziunea). Ulterior au fost introduşi şi alţi operatori
derivaţi (speciali). În acest context, operatorii din algebra relaţională pot fi grupaţi în două categorii:
operatori pe mulţimi şi operatori speciali.
Fie R1, R2, R3 - relaţii (tabele).

Operatorii pe mulţimi () sunt:


• Reuniunea. R3 = R1 ∪ R2, unde R3 va conţine tupluri din R1 sau R2 luate o singură dată;
• Diferenţa. R3 = R1 \ R2, unde R3 va conţine tupluri din R1 care nu se regăsesc în R2;
• Produsul cartezian. R3 = R1 × R2, unde R3 va conţine tupluri construite din perechi (x1,x2),
cu x1∈R1 şi x2∈R2;
• Intersecţia. R3 = R1 ∩ R2, unde R3 va conţine tupluri care se găsesc în R1 şi R2 în acelaşi
timp etc.

Operatorii relaţionali speciali sunt:


• Selecţia. Din R1 se obţine o subtabelă R2, care va conţine o submulţime din tuplurile iniţiale
din R1 ce satisfac un predicat (o condiţie). Numărul de atribute din R2 este egal cu
numărul de atribute din R1. Numărul de tupluri din R2 este mai mic decât numărul de
tupluri din R1;
• Proiecţia. Din R1 se obţine o subtabelă R2, care va conţine o submulţime din atributele
iniţiale din R1 şi fără tupluri duplicate. Numărul de atribute din R2 este mai mic decât
numărul de atribute din R1;
• Joncţiunea este o derivaţie a produsului cartezian, ce presupune utilizarea unui calificator care
să permită compararea valorilor unor atribute din R1 şi R2, iar rezultatul în R3. R1 şi R2
trebuie să aibă unul sau mai multe atribute comune care au valori comune.

b) Calculul relaţional se bazează pe calculul predicatelor de ordinul întâi şi a fost propus de E.F.
Codd. Predicatul este o relaţie care se stabileşte între anumite elemente şi care poate fi confirmată sau
nu. Predicatul de ordinul 1 este o relaţie care are drept argumente variabile care nu sunt predicate.
Variabila poate fi de tip tuplu (valorile sunt dintr-un tuplu al unei tabele) sau domeniu (valorile sunt
dintr-un domeniu al unei tabele). Cuantificatorii (operatorii) utilizaţi în calculul relaţional sunt:
universal (∀) şi existenţial (∃).

Construcţia de bază în calculul relaţional este expresia relaţională de calcul tuplu sau domeniu.

Expresia relaţională de calcul este formată din:


• operaţia de efectuat;
• variabile (tuplu respectiv domeniu);
• condiţii (de comparaţie, de existenţă);
• formule bine definite (operanzi-constante, variabile, funcţii, predicate, operatori);
• cuantificatori.

Pentru implementarea acestor operatori există comenzi specifice în limbajele de manipulare a datelor
(LMD) din sistemele de gestiune a bazelor de date relaţionale (SGBDR). Aceste comenzi sunt utilizate
în operaţii de regăsire (interogare).

61
După tehnica folosită la manipulare, LMD sunt bazate pe:
• calculul relaţional (QUEL în Ingres, ALPHA propus de Codd);
• algebra relaţională (ISBL, RDMS);
• transformare (SQL, SQUARE);
• grafică (QBE, QBF).

Transformarea oferă o putere de regăsire echivalentă cu cea din calculul şi algebra relaţională. Se
bazează pe transformarea (mapping) unui atribut sau grup de atribute într-un atribut dorit prin
intermediul unor relaţii. Rezultatul este o relaţie (tabelă) care se poate utiliza într-o altă transformare.

Grafica oferă interactivitate mare pentru constrirea cererilor de regăsire. Utilizatorul specifică cererea
alegând sau completând un ecran structurat grafic. Poate fi folosită de către toate categoriile de
utilizatori în informatică.

Algebra relaţională este prin definiţie neprocedurală (descriptivă), iar calculul relaţional permite o
manieră de căutare mixtă (procedurală/neprocedurală).

3) Restricţii de integritate ale modelului relaţional


Restricţiile de integritate ale modelului relaţional reprezintă cerinţe pe care trebuie să le îndeplinească
datele din cadrul bazei de date pentru a putea fi considerate corecte şi coerente în raport cu lumea reală
pe care o reflectă. Dacă o bază de date nu respectă aceste cerinţe, ea nu poate fi utilizată cu un maxim
de eficienţă.

Restricţiile sunt de două tipuri: restricţii de integritate structurală și restricţii de integritate de


comportament.

Restricţii de integritate structurale, care se definesc prin egalitatea sau inegalitatea unor valori din
cadrul relaţiilor. Acestea sunt:
• restricţia de unicitate a cheii - cheia primară trebuie să fie unică și minimală;
• restricţia de integritate a referirii. Într-o tabelă t1 care referă o tabelă t2, valorile cheii externe
trebuie să figureze printre valorile cheii primare din t2 sau să ia valoarea NULL
(neprecizat);
• restricţia de integritate a entităţii. Într-o tabelă, atributele din cheia primară nu trebuie să ia
valoarea NULL.

Cele trei restricţii de mai sus sunt minimale.

Pe lângă acestea, există o serie de alte restricţii structurale care se referă la dependenţele dintre date:
funcţionale, multivaloare, joncţiune etc. (sunt luate în considerare la tehnicile de proiectare a bazelor
de date relaţionale - BDR).

Restricţii de integritate de comportament - sunt cele care se definesc prin comportamentul datelor şi
ţin cont de valorile din BDR. Acestea sunt:
• restricţia de domeniu. Domeniul corespunzător unui atribut dintr-o tabelă trebuie să se
încadreze între anumite valori;
• restricţii temporale. Valorile anumitor atribute se compară cu nişte valori temporale (rezultate
din calcule etc.).

Restricţiile de comportament fiind foarte generale se gestionează fie la momentul descrierii datelor (de
exemplu prin clauza CHECK), fie în afara modelului la momentul execuţiei.

62
2.4. Tipuri de constrângeri de integritate

Restricţiile de integritate suportate de Oracle sunt:


• NOT NULL - nu permite valori NULL în coloanele unei tabele;
• UNIQUE - nu sunt permise valori duplicat în coloanele unei tabele;
• PRIMARY KEY - nu permite valori duplicate sau NULL în coloana sau coloanele definite
astfel;
• FOREIGN KEY - presupune că fiecare valoare din coloana sau setul de coloane definit astfel
să aibă o valoare corespondentă identică în tabela de legătură, tabelă în care coloana
corespondentă este definită cu restricţia UNIQUE sau PRIMARY KEY;
• CHECK - elimină valorile care nu satisfac anumite cerinţe (condiţii) logice.

Termenul de cheie (keys) este folosit pentru definirea câtorva categorii de constrângeri, şi sunt:
PRIMARY KEY, UNIQUE KEY, FOREIGN KEY, REFERENCED KEY.

3. Proiectarea bazelor de date relaţionale


Proiectarea BDR se realizează prin proiectarea schemelor BDR şi proiectarea modulelor funcţionale
specializate.

A. Proiectarea schemelor BDR


Schemele bazei de date sunt: conceptuală, externă şi internă.

a) Proiectarea schemei conceptuale porneşte de la identificarea setului de date necesar sistemului.


Aceste date sunt apoi integrate şi structurate într-o schemă (exemplu: pentru BDR relaţionale cea mai
utilizată tehnică este normalizarea). Pentru acest lucru se parcurg paşii:
• stabilirea schemei conceptuale iniţiale - care se deduce din modelul entitate-asociere. Pentru
acest lucru, se transformă fiecare entitate din model într-o colecţie de date (fişier), iar
pentru fiecare asociere se definesc cheile aferente. Dacă rezultă colecţii izolate, acestea se
vor lega de alte colecţii prin chei rezultând asocieri (1:1, 1:M, M:N);
• ameliorarea progresivă a schemei conceptuale - prin eliminarea unor anomalii (exemplu: cele
cinci forme normale pentru BDR relaţionale);
• stabilirea schemei conceptuale finale - trebuie să asigure un echilibru între cerinţele de
actualizare şi performanţele de exploatare (exemplu: o formă normală superioară asigură
performanţe de actualizare, dar timpul de răspuns va fi mai mare).

Tehnica de normalizare
Tehnica de normalizare este utilizată în activitatea de proiectare a structurii BDR şi constă în
eliminarea unor anomalii (neajunsuri) de actualizare din structură.

Anomaliile de actualizare sunt situaţii nedorite care pot fi generate de anumite tabele în procesul
proiectării lor:
• anomalia de ştergere - stergând un tuplu dintr-o tabelă, pe lângă informaţiile care trebuie
şterse, se pierd şi informaţiile utile existente în tuplul respectiv;
• anomaliile de adăugare - nu pot fi incluse noi informaţii necesare într-o tabelă, deoarece nu se
cunosc şi alte informaţii utile (de exemplu valorile pentru cheie);
• anomalia de modificare - este dificil de modificat o valoare a unui atribut atunci când ea apare
în mai multe tupluri.

63
Normalizarea este o teorie construită în jurul conceptului de forme normale (FN), care ameliorează
structura BDR prin înlăturarea treptată a unor neajunsuri şi prin imprimarea unor facilităţi sporite
privind manipularea datelor.

Normalizarea utilizează ca metodă descompunerea (top-down) unei tabele în două sau mai multe
tabele, păstrând informaţii (atribute) de legătură.

Codd a definit iniţial 3 forme normale, notate prin FN1, FN2 şi FN3. Întrucât într-o primă formulare,
definiţia FN3 ridică ceva probleme, Codd şi Boyce au elaborat o nouă variantă, cunoscută sub numele
de Boyce-Codd Normal Form (BCNF). Astfel BCNF este reprezentată separat în majoritatea lucrărilor.
R. Fagin a tratat cazul FN4 şi FN5.

O relaţie este într-o formă normală dacă satisface o mulţime de constrângeri.

Normalizarea bazei de date relaţionale poate fi imaginată ca un proces prin care pornindu-se de la
relaţia iniţială/universală R se realizează descompunerea succesivă a acesteia în subrelaţii, aplicând
operatorul de proiecţie. Relaţia R poate fi ulterior reconstruită din cele n relaţii obţinute în urma
normalizării, prin operaţii de joncţiune.

3.1. Formele normale: FN1; FN2; FN3

Prima formă normală (FN1)


FN1 este strâns legată de noţiunea de atomicitate a atributelor unei relaţii. Astfel, aducerea unei relaţii
în FN1 presupune introducerea noţiunilor de:
• atribut simplu (atribut atomic) - atribut care nu mai poate fi descompus în alte atribute, în caz
contrar, atributul este compus (atribut neatomic);
• atribut compus;
• grupuri repetitive de atribute - atribut (grup de atribute) dintr-o relaţie care apare cu valori
multiple pentru o singură apariţie a cheii primare a relaţiei nenormalizate.

Aducerea unei relaţii universale în FN1


FN1 este tratată în general cu superficialitate, deoarece principala cerinţă – atomicitatea valorilor –
este uşor de îndeplinit (cel puţin la prima vedere).

Dintre toate formele normale, doar FN1 are caracter de obligativitate. Se spune că o bază de date este
normalizată dacă toate relaţiile se află măcar în FN1.

O relaţie este în FN1 dacă domeniile pe care sunt definite atributele relaţiei sunt constituite numai din
valori atomice. Un tuplu nu trebuie să conţină atribute sau grupuri de atribute repetitive.

Aducerea relaţiilor în FN1 presupune eliminarea atributelor compuse şi a celor repetitive.

Se cunosc trei soluţii pentru determinarea grupurilor repetitive:


• eliminarea grupurilor repetitive pe orizontală (în relaţia R iniţială, în locul atributelor compuse
se trec componentele acestora, ca atribute simple);
• eliminarea grupurilor repetitive prin adăugarea de tupluri;
• eliminarea grupurilor repetitive prin construirea de noi relaţii.

Primele două metode generează relaţii stufoase prin duplicarea forţată a unor atribute, respectiv
tupluri, creându-se astfel redundanţe masive cu multiple anomalii de actualizare.

64
Metoda a treia presupune eliminarea grupurilor repetitive prin construirea de noi relaţii, ceea ce
generează o structură ce oferă cel mai mic volum de redundanţă.

Exemplu: Fie relaţia nenormalizată (primară) FACTURI. Dorim să stabilim o structură de tabele care
să permită stocarea informaţiilor conţinute în document (factură) şi obţinerea unor situaţii sintetice
privind evidenţa sumelor facturate pe produse, pe clienţi, pe anumite perioade de timp.

FACTURI

nr_factura#
data_factura
nume_client
adresa_client
telefon_client
email_client
banca_client
nr_cont_client
delegat
cod_produs
denumire_produs
unitate_de_masura
data_expirarii
cantitate
pret_unitar
valoare
valoare_tva
total_valoare_factura
total_valoare_tva
Relaţia FACTURI nenormalizată

În cazul în care o factură conţine mai multe produse, relaţia de mai sus va avea grupurile repetitive:
„cod_produs”, „denumire_produs”, „data_expirarii”, „cantitate”, „pret_unitar”, „valoare”,
„valoare_tva”.

Etapele de aducere a unei relaţii în FN1 sunt:


I. se construieşte câte o relaţie pentru fiecare grup repetitiv;
II. în schema fiecărei noi relaţii obţinute la pasul 1 se introduce şi cheia primară a relaţiei R
nenormalizată;
III. cheia primară a fiecărei noi relaţii va fi compusă din atributele chei ale relaţiei R, plus unul
sau mai multe atribute proprii.

Exemplu: Deoarece o factură poate avea unul sau mai multe produse înscrise pe aceasta, informaţiile
legate de produse vor fi separate într-o altă tabelă. Aplicând etapele de aducere în FN1, se obţin două
relaţii.

65
FACTURI LINII_FACTURI

nr_factura# nr_factura#
data_factura cod_produs#
nume_client denumire_produs
adresa_client unitate_de_masura
telefon_client data_expirarii
banca_client cantitate
nr_cont_client pret_unitar
delegat valoare
toal_valoare_factura valoare_tva
toal_valoare_tva
Relaţia FACTURI adusă în forma normală FN1

Observaţii:
• Câmpul „adresa_client” cuprinde informaţii despre judeţul, localitatea, strada şi numărul
domicililului clientului. Dacă se consideră că este de interes o evidenţă a sumelor
factorizate pe judeţe sau localităţi, se vor pune în locul câmpului „adresa_client” trei
câmpuri distincte: „judet_client”, „localitate_client”, „adresa_client”, uşurând în acest fel
interogările;
• Între tabela FACTURI şi tabela LINII_FACTURI există o relaţie de „una-la-multe”, adică
unui număr unic de factură îi pot corespunde unul sau mai multe produse care sunt
memorate ca înregistrări în tabela LINII_FACTURI. Cheia primară în această tabelă este
o cheie compusă, formată din două câmpuri: „nr_factura” şi „cod_produs”.

Însă eliminarea grupurilor repetitive, adică aducerea unei relaţii în FN1, nu rezolvă complet problema
normalizării.

A doua formă normală (FN2)


FN2 este strâns legată de noţiunea de dependenţă funcţională (DF).

O relaţie se află în a doua formă normală (FN2) dacă:


1. se află în forma normală FN1 şi
2. fiecare atribut care nu este cheie este dependent de întreaga cheie primară.

Etapele de aducere a unei relaţii de la FN1 la FN2 sunt:


I. se identifică posibila cheie primară a relaţiei aflate în FN1;
II. se identifică toate dependenţele dintre atributele relaţiei, cu excepţia acelora în care sursa este
un atribut component al cheii primare;
III. se identifică toate dependenţele care au ca sursă un atribut sau subansamblu de atribute din
cheia primară;
IV. pentru fiecare atribut (sau subansamblu) al cheii de la pasul III se creează o relaţie care va
avea cheia primară atributul (subansamblul) respectiv, iar celelalte atribute vor fi cele care
apar ca destinaţie în dependenţele de la etapa III.

Exemplu: Relaţia care conţine date redundante (de exemplu, modificarea denumirii unui produs atrage
după sine modificarea în fiecare tuplu în care apare acest produs) este relaţia LINII_FACTURI. Se
observă că nu există nici o dependenţă funcţională între atributele necomponente ale cheii. În schimb,
toate atributele care nu intră în alcătuirea cheii compuse sunt dependente de aceasta, iar DF dintre
66
atributul component al cheii primare sunt: cod_produs --> denumire_produs, cod_produs -->
unitate_de_masura, data_expirarii. Ca urmare se formează încă două relaţii.

FACTURI LINII_FACTURI PRODUSE

nr_factura# nr_factura# cod_produs#


data_factura cod_produs# denumire_produs
nume_client cantitate unitate_de_masura
adresa_client pret_unitar data_expirarii
telefon_client valoare
email_client valoare_tva
banca_client
nr_cont_client
delegat
toal_valoare_factura
toal_valoare_tva
Relaţia FACTURI în a doua forma normală FN2

Chiar dacă au fost eliminate o parte din redundanţe, mai rămân şi alte redundanţe ce se vor elimina
aplicând alte forme normale.

A treia formă normală (FN3)


O relaţie este în forma normală trei (FN3) dacă:
1. se găseşte în FN2 şi
2. fiecare atribut care nu este cheie (nu participă la o cheie) depinde direct de cheia primară. A
treia regulă de normalizare cere ca toate câmpurile din tabele să fie independente între ele.

Exemplu: În relaţia FACTURI se observă că atributul „nume_client” determină în mod unic atributele
„adresa_client”, „telefon_client”, „email_client”, „banca_client” şi „nr_cont_client”. Deci pentru
atributul „nume_client” se construieşte o relaţie CLIENTI în care cheia primară va fi acest atribut, iar
celelalte atribute vor fi „adresa_client”, „telefon_client”, „email_client”, „banca_client” şi
„nr_cont_client”. Câmpurile „valoare” şi „valoare_tva” depind de câmpurile „cantitate”, „pret_unitar”,
şi de un procent fix de TVA. Fiind câmpuri ce se pot calcula în orice moment, ele vor fi eliminate din
tabelă LINII FACTURI, deoarece constituie informaţie memorată redundant.

FACTURI LINII_FACTURI PRODUSE CLIENTI

nr_factura# nr_factura# cod_produs# nume_client#


data_factura cod_produs# denumire_produs adresa_client
nume_client cantitate unitate_de_masura telefon_client
delegat pret_unitar data_expirarii email_client
toal_valoare_factura banca_client
toal_valoare_tva nr_cont_client

Relaţia FACTURI în a treia forma normală FN3

Etapele de aducere a unei relaţii de la FN2 la FN3 sunt:


I. se identifică toate atributele ce nu fac parte din cheia primara şi sunt surse ale unor dependenţe
funcţionale;
67
II. pentru aceste atribute, se construieşte câte o relaţie în care cheia primară va fi atributul
respectiv, iar celelalte atribute, destinaţiile din DF considerate;
III. din relaţia de la care s-a pornit se elimină atributele destinaţie din DF identificată la pasul I,
păstrându-se atributele surse.

Observaţii:
• Această a treia formă normală mai poate suferi o serie de rafinări pentru a putea obţine o
structură performantă de tabele ale bazei de date. De exemplu se observă că
„nume_client” este un câmp în care este înscris un text destul de lung format dintr-o
succesiune de litere, semne speciale (punct, virgulă, cratimă), spaţii, numere. Ordonarea şi
regăsirea informaţiilor după astfel de câmpuri este lentă şi mai greoaie decât după câmpuri
numerice. Din acest motiv se poate introduce un nou atribut „cod_client” care să fie
numeric şi care să fie cheia primară de identificare pentru fiecare client;
• O altă observaţie care poate fi făcută în legătură cu tabelele aflate în cea de a treia formă
normală este aceea că „total_valoare_factura” este un câmp care ar trebui să conţină
informaţii sintetice obţinute prin însumarea valorii tuturor ofertelor aflate pe o factură.
Este de preferat ca astfel de câmpuri să fie calculate în rapoarte sau interogări şi să nu fie
memorate în tabelele bazei de date.

FACTURI LINII_FACTURI PRODUSE CLIENTI

nr_factura# nr_factura# cod_produs# cod_client#


data_factura cod_produs# denumire_produs nume_client
nume_client cantitate unitate_de_masur adresa_client
delegat pret_unitar a telefon_client
data_expirarii email_client
banca_client
nr_cont_client

Verificarea aplicării corecte a procesului de normalizare se realizează astfel încât uniunea acestor
relaţii să producă relaţia iniţială, cu alte cuvinte, descompunerea este fără pierderi.
Celelalte forme normale se întâlnesc mai rar în practică. Aceste forme nu sunt respectate, în general,
pentru că beneficiile de eficienţă pe care le aduc nu compensează costul şi munca de care este nevoie
pentru a le respecta.

Forma normală Boyce Codd (FNBC)


O definiţie mai riguroasă pentru FN3 a fost dată prin forma intermediară BCNF (Boyce Codd Normal
Form):
• tabelă este în BCNF dacă fiecare determinant este un candidat cheie. Determinantul este un
atribut elementar sau compus faţă de care alte atribute sunt complet dependente
funcţional.

A patra formă normală (FN4)


O tabelă este în FN4 dacă şi numai dacă:
• este în FN3 şi
• nu conţine două sau mai multe dependenţe multivaloare. Într-o tabelă T, fie A, B, C trei
atribute. În tabela T se menţine dependenţa multivaloare A dacă şi numai dacă mulţimea
valorilor lui B ce corespunde unei perechi de date (A,C), depinde numai de o valoare a lui
A şi este independentă de valorile lui C.

68
A cincea formă normală (FN5) (numită şi forma normală proiecţie-uniune)
O tabelă este în FN5 dacă şi numai dacă:
• este în FN4 şi
• orice dependenţă de uniune a lui R este o consecinţă a unei chei candidat a lui R. În tabela T
(A,B,C) se menţine dependenţa joncţiune (AB,AC) dacă şi numai dacă T menţine
dependenţa multivaloare A -->> B sau C.

Fiecare dintre cele 6 forme normale este mai restrictivă ca predecesoarea sa. Astfel, de exemplu, o
schemă de relaţie aflată în forma normală trei este şi în forma normală doi, aşa cum se reprezintă în
figura de mai jos:

FN 5
FN 1 FN 3 FNBC FN 4
FN 2

Forme normale

Scopul formelor normale este acela de a elimina redundanţele din cadrul relaţiilor prin descompunerea
acestora în două sau mai multe relaţii, fără însă a pierde informaţie, ceea ce înseamnă faptul că este
posibilă, în orice moment, revenirea la relaţia originară doar pe baza relaţiilor obţinute din
descompunere.

Dependenţa multivaloare este caz particular al dependenţei joncţiune. Dependenţa funcţională este caz
particular al dependenţei multivaloare.

b) Proiectarea schemei externe are rolul de a specifica viziunea fiecărui utilizator asupra BDR. Pentru
acest lucru, din schema conceptuală se identifică datele necesare fiecărei viziuni. Datele obţinute se
structurează logic în subscheme ţinând cont de facilităţile de utilizare şi de cerinţele utilizator. Schema
externă devine operaţională prin construirea unor vizualizări (view) cu SGBD-ul şi acordarea
drepturilor de acces. Datele într-o vizualizare pot proveni din una sau mai multe colecţii şi nu ocupă
spaţiul fizic.

c) Proiectarea schemei interne presupune stabilirea structurilor de memorare fizică a datelor şi


definirea căilor de acces la date. Acestea sunt specifice fie SGBD-ului (scheme de alocare), fie
sistemului de operare. Proiectarea schemei interne înseamnă estimarea spaţiului fizic pentru BDR,
definirea unui model fizic de alocare (trebuie văzut dacă SGBD-ul permite explicit acest lucru) şi
definirea unor indecşi pentru accesul direct, după cheie, la date.

B. Proiectarea modulelor funcţionale specializate


Proiectarea modulelor funcţionale ţine cont de concepţia generală a BDR, precum şi de schemele
proiectate anterior. În acest sens, se proiectează fluxul informaţional, modulele de încărcare şi
manipulare a datelor, interfeţele specializate, integrarea elementelor proiectate cu organizarea şi
funcţionarea BDR.

69
Realizarea componentelor logice. Componentele logice ale unei BD sunt programele de aplicaţie
dezvoltate, în cea mai mare parte, în SGBD-ul ales. Programele se realizează conform modulelor
funcţionale proiectate în etapa anterioară. Componentele logice ţin cont de ieşiri, intrări, prelucrări şi
de colecţiile de date. În paralel cu dezvoltarea programelor de aplicaţii se întocmesc şi diferite
documentaţii (tehnică, de exploatare, de prezentare).

Punerea în funcţiune şi exploatarea. Se testează funcţiile BDR mai întâi cu date de test, apoi cu date
reale. Se încarcă datele în BDR şi se efectuează procedurile de manipulare, de către beneficiar cu
asistenţa proiectantului. Se definitivează documentaţiile aplicaţiei. Se intră în exploatare curentă de
către beneficiar conform documentaţiei.

Dezvoltarea sistemului. Imediat după darea în exploatare a BDR, în mod continuu, pot exista factori
perturbatori care generează schimbări în BDR. Factorii pot fi: organizatorici, datoraţi progresului
tehnic, rezultaţi din cerinţele noi ale beneficiarului, din schimbarea metodologiilor etc.

4. Limbajul SQL (Structured Query Language)


Limbajul SQL (Structured Query Language) este limbajul utilizat de majoritatea sistemelor de baze de
date relaţionale (SGBDR) pentru definirea şi manipularea datelor.

4.1. Structura lexicală a limbajului SQL

Elementele unei instrucţiuni (statement) sunt:


• cuvinte cheie (key words) - dintre care fac parte comenzile (SELECT, UPDATE, INSERT
etc), operatorii (AND, OR, NOT, LIKE), clauzele (WHERE, SET, VALUES etc);
• identificatori (identifier) - sunt elementele care denumesc tabela, coloana sau alt obiect al BD.
SQL face diferenţa între literele mari şi mici, deci este „case-sensitive”; identificatorul
care conţine ghilimele se numeşte identificator delimitat;
• constante (literali) - reprezintă şiruri de caractere (‘ ‘), numere întregi, numere reale (ex. 3.5;
4.; .001; 5e2), constanta NULL, constante de tip logic (1 pentru TRUE şi 0 pentru
FALSE);
• caractere speciale - cum ar fi (;) care semnifică terminarea comenzilor; (.) care semnifică
virgula zecimală; sau (*) care simbolizează operatorul de înmulţire.

4.2. Operatori SQL

SQL are următorii operatori:


• operatori aritmetici binari:
+
-
*
% modulo
^ ridicarea la putere
& AND orientat pe biţi
| OR orientat pe biţi
# XOR orientat pe biţi
<< deplasare la stânga
>> deplasare la dreapta

70
• operatori binari de comparaţie:
<
>
<=
>=
=
<> sau != diferit
• operatori aritmetici mari:
@ valoarea absolută
! factorial
!! factorial, operator postfix
~ NOT orientat pe biţi
• operatori de comparaţie:
A BETWEEN min AND max (compară A cu două valori: min şi max)
A IN (v1,...,vn) compară A cu o listă de valori
A IS NULL
A IS NOT NULL
A LIKE model_şir
• operatori logici:
Operatorii logici sunt legaţi prin cuvintele cheie AND, OR, NOT şi returnează o valoare
logică TRUE, FALSE sau NULL.
• operatori relaţionali:
UNION (reuniune)
INTERSECT (intersecţie)
MINUS (diferenţă)

4.3. Funcţii definite în SQL

Categorii de funcţii SQL:


• funcții pe un sigur rând (funcţii scalare) - realizează operații asupra unui singur rând și
returnează un singur rezultat pentru fiecare rând. Funcțiile pe un sigur rând cuprind
următoarele tipuri de funcții:
o funcții de tip caracter - acceptă argumente de tip caracter și întorc rezultate de tip
caracter (CHR, CONCAT, INITCAP, LOWER, LPAD, LTRIM, REPLACE, RPAD,
RTRIM, SUBSTR, UPPER etc.) sau numeric (ASCII, INSTR, LENGTH);
o funcții de tip numeric (de calcul trigonometric: sin, cos, tg, ctg etc.; de calcul al
logaritmului: ln, log, lg; de calcul al puterilor: pow; de rotunjire: floor, ceil etc.) -
acceptă argumente de tip numeric și întorc rezultate de tip numeric;
o funcții de tip dată calendaristică (ADD_MONTHS, LAST_DAY, MONTHS_
BETWEEN, NEXT_DAY, SYSDATE etc.) - acceptă argumente de tip dată
calendaristică și întorc rezultate de tip dată calendaristică cu excepția funcției
MONTH_BEETWEEN care întoarce o valoare numerică;
o funcții de conversie (TO_CHAR, TO_NUMBER, TO_DATE, CAST) - fac conversia
dintr-un tip de dată în altul;
o funcții generale: NVL, DECODE.

• funcții pe mai mutle rânduri (funcții de grup) - lucrează cu grupuri de rânduri pentru a returna
un singur rezultat pentru fiecare grup. Aceste funcții sunt cunoscute cu denumirea de
funcții de grup. Toate funcţiile de grup, mai puţin COUNT(*) ignoră valorile NULL.
Majoritatea funcţiilor de grup acceptă opţiunile: DISTINCT (determină luarea în calcul

71
numai a valorilor distincte ale rândurilor din cadrul grupului) şi ALL (este implicit și
determină luarea în calcul a tuturor valorilor grupului de rânduri).

Funcţiile agregat - calculează un rezultat din mai multe linii ale unui tabel (funcţii de totalizare):
COUNT (furnizează numărul de linii ale unui rezultat);
SUM (execută suma tuturor valorilor dintr-o coloană);
MAX (returnează valoarea cea mai mare dintr-o coloană);
MIN (returnează valoarea cea mai mică dintr-o coloană);
AVG (calculează media valorilor dintr-o coloană).

Aceste funcţii vor fi folosite în instrucţiunea SELECT.

Funcţiile scalare - primesc unul sau mai multe argumente şi returnează valoarea calculată sau NULL
în caz de eroare. Argumentele funcţiilor pot fi constante sau valori ale atributelor specificate prin
numele coloanelor corespunzătoare.

4.4. Tipuri de date

În limbajul SQL sunt definite mai multe tipuri de date: numeric, şir de caractere, şir de biţi, dată
(calendaristică), timp.

Denumirile tipurilor de date precum şi limitele acestora diferă de la un SGBD la altul, dar în general,
sunt destul de asemănătoare.

• Tipul numeric include


 numere întregi:
o INTEGER sau INT reprezentat pe 4 octeţi;
o SMALLINT reprezentat pe 2 octeţi;
 numere reale reprezentate în virgulă flotantă, cu diferite precizii:
o FLOAT reprezentat pe 4 octeţi;
o REAL reprezentat pe 8 octeţi;
o DOUBLE [PRECISION] reprezentat pe 8 octeţi;
 numere zecimale reprezentate cu precizia dorită:
o tipul NUMERIC sau DECIMAL, cu forma numeric[(p,s)], unde p este numărul total
de cifre afişate, iar s este numărul de cifre după punctul zecimal.

• Tipul şir de caractere


o CHARACTER (n) sau CHAR (n) definesc şiruri de caractere cu lungime fixă;
o CHARACTER VARYING sau VARCHAR (n) defineşte şirul de caractere cu
lungime variabilă.

Asemănarea dintre cele două tipuri prezentate mai sus este aceea că ambele reprezintă şiruri de
maxim n caractere, iar deosebirea este aceea că pentru şiruri cu număr de caractere mai mic ca n,
CHAR (n) completează şirul cu spaţii albe până la n caractere, iar VARCHAR (n) memorează
numai atâtea caractere câte are şirul dat.

• Tipul şiruri de biţi


o BIT(n) defineşte secvenţe de cifre binare (care pot lua valoarea 0 sau 1) de lungime
finită n;
o BIT VARYING (n) defineşte secvenţe de lungime variabilă, cu limita maximă n.

72
• Tipuri pentru data calendaristică şi timp
o DATE permite memorarea datelor calendaristice în formatul yyyy-mm-dd;
o TIME permite memorarea timpului, folosind trei câmpuri hh:mm:ss;
o TIMESTAMP(p) permite memorarea combinată a datei calendaristice şi a timpului,
cu precizia p pentru câmpul SECOND (al secundelor); valoarea implicită a lui p este
6;
o INTERVAL este utilizat pentru memorarea intervalelor de timp.

Tipurile de date sunt „case-insensitive”, deci nu ţin cont de caracterele mari sau mici.

4.5. Categorii de instrucţiuni SQL

În funcţie de tipul acţiunii pe care o realizează, instrucţiunile SQL se împart în mai multe categorii.

Datorită importanţei pe care o au comenzile componente, unele dintre aceste categorii sunt evidenţiate
ca limbaje relaționale în cadrul SQL, şi anume:
• limbajul de definire a datelor (LDD sau DDL – Data Definition Language);
• limbajul de interogare a datelor (LQD sau DQL - Data Query Language);
• limbajul de prelucrare a datelor (LMD sau DML – Data Manipulation Language);
• limbajul de control al datelor (LCD sau DCL – Data Control Language).

Pe lângă comenzile care alcătuiesc aceste limbaje, SQL cuprinde:


• instrucţiuni pentru controlul sesiunii;
• instrucţiuni pentru controlul sistemului;
• instrucţiuni SQL încapsulate.

5. Limbajul de definire a datelor (LDD)


Limbajele relaționale de definire a datelor oferă următoarele facilități utilizatorilor:
• facilități de descriere a datelor la nivel conceptual. În vederea descrierii datelor la nivel
conceptual, limbajele relaționale conțin o serie de comenzi, și anume:
o crearea unei BD (dicționarul BD): CREATE DATABASE;
o ștergerea unei BD: DROP DATABASE;
o crearea tabelelor de bază: CREATE TABLE;
o ștergerea tabelelor de bază: DROP TABLE;
o crearea de sinonime: CREATE SYNONYM;
o ștergerea sinonimelor: DROP SYNONYM;
o actualizarea structurii unei tabele: ALTER TABLE cu opțiunile ADD, MODIFY,
DROP;
o adăugarea restricțiilor de integritate: ASSERT ON. În Oracle restricțiile de integritate
sunt: NULL, CHECK, pe cheie (PRIMARY, UNIQUE, REFERENTIAL).

• facilități de descriere a datelor la nivel logic. Pentru descrierea datelor la nivel logic,
limbajele relaționale dispun de o serie de comenzi, precum:
o crearea tabelelor virtuale: CREATE VIEW;
o ștergerea tabelelor virtuale: DROP VIEW;
o acordarea drepturilor de acces la BD:
 GRANT CONNECT – conectarea la BD a unui utilizator;
 GRANT drepturi – acordarea unor drepturi de acces (pentru regăsire,
actualizare etc.).

73
o retragerea drepturilor de acces la BD:
 REVOKE drepturi – retragerea unor drepturi;
 REVOKE CONNECT – deconectarea unui utilizator de la BD.

 facilități de descriere a datelor la nivel fizic. Pentru definirea unor caracteristici legate de
organizarea la nivel fizic a datelor din baza de date, limbajele relaționale dispun de o serie
de comenzi, și anume:
o crearea indecşilor: CREATE INDEX;
o ștergerea indecşilor: DROP INDEX;
o controlul alocării spațiului fizic al BD:
 CREATE SPACE – creează un model de alocare a spațiului fizic pentru o
BD;
 ALTER SPACE – actualizează modelul de alocare a spațiului fizic;
 DROP SPACE – şterge un model de alocare a spațiului fizic.
o regruparea fizică a datelor dintr-o BD (clustere):
 CREATE CLUSTER – creează un cluster dintr-o BD;
 ALTER CLUSTER– actualizează un cluster;
 DROP CLUSTER – şterge un cluster.

5.1. Comenzi (CREATE, ALTER, DROP)

Limbajul de definire a datelor (a schemei unei BD) include instrucţiuni ce permit:


 crearea schemei bazei de date;
 adăugarea relaţiilor la schema bazei;
 ştergerea unor relaţii existente;
 adăugarea de noi atribute relaţiilor existente;
 optimizarea bazei de date (index, grup, declanşator);
 definirea structurii fizice şi logice a unei BD;
 restricţii cu privire la utilizarea structurii.

Comenzi pentru crearea unei baze de date


CREATE DATABASE nume_baza;

Comenzi pentru suprimarea unei baze de date


DROP DATABASE nume_baza;
Această comandă distruge BD cu numele nume_baza.

Comenzi pentru crearea relaţiilor de bază


În cadrul acestor comenzi se precizează numele relaţiei precum şi numele şi tipul atributelor.
CREATE TABLE nume_tabela (atribute);

Crearea unei relaţii indicând cheia la nivel de coloană


Exemplu: Să se creeze relaţia PRODUSE(cod_produs, denumire_proddus,
unitate_de_masura, data_expirarii).
CREATE TABLE PRODUSE
(cod_produs VARCHAR(5) PRIMARY KEY,
denumire_proddus VARCHAR(30),
unitate_de_masura VARCHAR(2),
data_expirarii DATE);

74
Crearea unei relaţii indicând cheile la nivel de tabel
Exemplu: Să se creeze relaţia LINII_FACTURI(nr_factura, cod_produs, cantitate,
PRET_UNIRAR).
CREATE TABLE LINII_FACTURI
(nr_factura VARCHAR(5),
cod_produs CHAR (5),
cantitate REAL,
pret_unitar REAL,
PRIMARY KEY (nr_factura, cod_produs),
FOREIGN KEY (cod_produs)
REFERENCES PRODUSE (cod_produs));
Dacă cheia primară are mai mult de o coloană atunci cheile trebuie indicate la nivel de tabel.

Crearea unui tabel prin copiere


Exemplu: Să se creeze relaţia PRODUSE_PAINE(cod_produs, denumire_proddus,
unitate_de_masura, data_expirarii), utilizând copierea datelor din relaţia PRODUSE.
CREATE TABLE PRODUSE_PAINE
SELECT cod_produs, denumire_proddus, unitate_de_masura,
data_expirarii
FROM PRODUSE
WHERE denumire_proddus LIKE 'PAINE';

Comenzi pentru suprimarea unei relaţii de bază


DROP TABLE nume_tabela;
Comanda SQL distruge relaţia nume_tabela.

Comenzi pentru schimbarea numelui unei relaţii


RENAME nume_tabela TO nume_tabela_nou;
Exemplu: Să se modifice numele relaţiei PRODUSE_PAINE în PROD_PAINE, apoi să se suprime
relaţia PROD_PAINE.
RENAME TABLE PRODUSE_PAINE TO PROD_PAINE;
DROP TABLE PROD_PAINE;

Comenzi pentru modificarea structurii unei relaţii


Prin modificarea structurii unei relaţii se înţelege:
• extinderea schemei relaţiei prin adăugarea de noi atribute;
• restrângerea schemei unei relaţii prin suprimarea unor atribute;
• modificarea numelui şi/sau tipului unui atribut din cadrul relaţiei.

Unele limbaje relaţionale (QBE) admit toate aceste tipuri de modificări în schema unei relaţii, iar
altele (SQL sau QUEL) numai o parte.
ALTER TABLE nume_tabel ...

Adăugarea unui atribut cu ajutorul opţiunii ADD


Exemplu: Să se adauge atributul „TOTAL_VALOARE_FACTURA” la relaţia LINII_FACTURI.
ALTER TABLE LINII_FACTURI
ADD (TOTAL_VALOARE_FACTURA REAL);

Modificarea unui atribut cu ajutorul opţiunii MODIFY


Exemplu: Să se modifice forma preţului unitar din relaţia LINII_FACTURI.

75
ALTER TABLE LINII_FACTURI
MODIFY pret_unitar DECIMAL (10,2);

Comenzi pentru declararea restricţiilor de integritate (a constrângerilor)


Constrângerea este un mecanism care asigură că valorile unei coloane sau a unei mulţimi de coloane
satisfac o condiţie declarată. Unei constrângeri i se poate da un nume unic. Dacă nu se specifică un
nume explicit atunci sistemul automat îi atribuie un nume de forma SYS_Cn, unde n reprezintă
numărul constrângerii. Constrângerile pot fi şterse, pot fi adăugate, pot fi activate sau dezactivate, dar
nu pot fi modificate.
Prin comanda CREATE TABLE pot fi specificate anumite restricţii (constrângeri) prin care se
exprimă o condiţie care trebuie respectată de toate tuplurile uneia sau mai multor relaţii. Acestea pot fi
definite cu ajutorul comenzii:
ALTER TABLE

Constrângerile declarative pot fi:


• constrângeri de domeniu, care definesc valorile luate de un atribut:
o DEFAULT
o NOT NULL
o UNIQUE
o CHECK
• constrângeri de integritate a entităţii, care precizează cheia primară:
PRIMARY KEY
• constrângeri de integritate referenţială, care asigură corespondenţa între cheile primare şi
cheile externe corespunzătoare:
FOREIGN KEY

Fiecărei restricţii i se poate da un nume, lucru util atunci când, la un moment dat (salvări, restaurări,
încărcarea BD) se doreşte dezactivarea uneia sau mai multora dintre acestea. Astfel se prefigurează
numele fiecărei restricţii cu tipul său:
• pk_(PRIMARY KEY) - pentru cheile primare;
• un_(UNIQUE) - pentru cheile alternative - care impune respectarea unicității valorilor;
• nn_(NOT NULL) - pentru atributele obligatorii;
• ck_(CHECK) - pentru reguli de validare la nivel de atribut;
• fk_(FOREIGN KEY) - pentru cheile străine.

Exemplu: Să se realizeze constrângerea de cheie primară, de cheie externă şi constrângerea de


domeniu pentru relaţia LINII_FACTURI.
CREATE TABLE LINII_FACTURI
(nr_factura VARCHAR(5) NOT NULL,
CONSTRAINT pk_nr PRIMARY KEY (nr_factura),
cod_produs CHAR (5),
CONSTRAINT FOREIGN KEY fk_co(cod_produs)
REFERENCES PRODUSE (cod_produs),
cantitate REAL,
PRET_UNIRAR REAL);

Observaţii:
• Liniile ce nu respectă constrângerea sunt depuse automat într-un tabel special;
• Constrângerile previn ştergerea unui tabel dacă există dependenţe;
• Constrângerile pot fi activate sau dezactivate în funcţie de necesităţi.
• Constrângerile pot fi create o dată cu tabelul sau după ce acesta a fost creat.
76
Modificarea unei restricţii de integritate
ALTER TABLE nume_tabela
MODIFY(nume_atribut TIP_CONSTRÂNGERE);

Exemplu: Să se modifice una din constrângerile din exemplul de mai sus:


ALTER TABLE LINII_FACTURI
MODIFY PRET_UNIRAR REAL NOT NULL;

Activarea şi/sau dezactivarea unei constrângeri


Activarea sau dezactivarea unei constrângeri se realizează cu ajutorul opţiunilor ENABLE sau
DISABLE.

Exemplu: Să se dezactiveze, apoi să se activeze constrângerea de cheie primară din relaţia


LINII_FACTURI.
ALTER TABLE LINII_FACTURI
ADD (CONSTRAINT pk_nr PRIMARY KEY (nr_factura) DISABLE);
ALTER TABLE LINII_FACTURI ENABLE (CONSTRAINT pk_nr);

Suprimarea unei constrângeri cu ajutorul opţiunii DROP


ALTER TABLE nume_tabela DROP PRIMARY KEY;

Exemplu: Să se suprime restricţia de cheie primară pentru atributul „nr_factura” din tabela
LINII_FACTURI.
ALTER TABLE LINII_FACTURI DROP PRIMARY KEY;

Adăugarea unei constrângeri cu ajutorul opţiunii ADD


ALTER TABLE nume_tabela ADD CONSTRAINT ...;

Exemplu: Să se adauge restricţia de cheie primară „nr_factura” pentru relaţia LINII_FACTURI.


ALTER TABLE LINII_FACTURI
ADD CONSTRAINT pk_nr PRIMARY KEY (nr_factura);

Observaţii:
• Comanda ALTER TABLE realizează modificarea structurii tabelului (la nivel de coloană sau
la nivel de tabel), dar nu modificarea conţinutului acestuia;
• Constrângerile pot fi adăugate (ADD CONSTRAINT), şterse (DROP CONSTRAINT),
activate (ENABLE) sau dezactivate (DISABLE), dar nu pot fi modificate;
• Dacă există o cheie externă care referă o cheie primară şi dacă se încearcă ştergerea cheii
primare, această ştergere nu se poate realiza (tabelele sunt legate prin declaraţia de cheie
externă). Ştergerea este totuşi permisă dacă în comanda ALTER apare opţiunea
CASCADE, care determină şi ştergerea cheilor externe ce referă cheia primară urmărind
sintaxa:
ALTER TABLE Nume_tabela
DROP PRIMARY KEY CASACDE;

77
6. Limbajul de manipulare a datelor (LMD)
Instrucţiunile LMD (sau DML, Data Manipulation Language) sunt utile pentru interogarea și
prelucrarea datelor din obiectele unei scheme. Aceste instrucţiuni permit:
• interogarea bazei de date (SELECT);
• adăugarea de înregistrări în tabele (sub forma rândurilor din tabele) sau vizualizări (INSERT);
• modificarea valorilor unor coloane din înregistrările existente în tabele sau vizualizări
(UPDATE);
• suprimarea de înregistrări din tabele sau vizualizări (DELETE).

O colecție de comenzi LMD care formează o unitate logică de lucru se numește tranzacție.

Instrucţiunile LMD individuale afectează datele dintr-un singur tabel. Este posibil ca într-o
instrucţiune LMD să se refere şi o vizualizare care conţine date din mai multe tabele (adică o
vizualizare care conţine o uniune de tabele), dar, în acest caz, instrucţiunea LMD poate referi numai
coloane dintr-un singur tabel al vizualizării. Cu alte cuvinte, atunci când o instrucţiune LMD foloseşte
o vizualizare, toate coloanele vizualizării referite în instrucţiunea LMD trebuie să corespundă unor
coloane dintr-un singur tabel fizic al bazei de date.

Sistemul SGBD nu va efectua în baza de date nici o modificare care încalcă una din restricţii.

La formarea instrucțiunilor LMD, trebuie să se ţină seama de următoarele aspecte referitoare la


restricţiile tabelului modificat:
• restricţii de tip cheie primară. Atunci când se inserează un nou rând într-un tabel, cheia
primară a noului rând trebuie să fie unică în întregul tabel. Când se modifică valoarea unei
chei primare (ceea ce se întâmplă rareori), noua valoare trebuie să fie unică în întregul
tabel;
• restricţii de unicitate. Ca şi în cazul cheilor primare, coloanele pe care a fost definită o
restricţie de unicitate trebuie sa aibă valori unice în întregul tabel;
• restricţii NOT NULL. O valoare nula (NULL) este o modalitate specială prin care sistemul
SGBD tratează valoarea unei coloane pentru a indica faptul că valoarea coloanei
respective nu este cunoscută. O valoare nulă nu este același lucru un un spațiu liber, un șir
vid sau valoarea zero – este o valoare specială care nu este egală cu nimic altceva.
În cazul instrucţiunilor INSERT, trebuie specificate valori pentru toate coloanele cu
restricţii NOT NULL.
În cazul instrucţiunilor UPDATE nu se pot înlocui valorile unei coloane cu valori nule
dacă pe coloana respectivă este definită o restricţie NOT NULL.
Dacă instrucţiunea LMD referă o vizualizare, nu poate fi folosită într-o instrucţiune
INSERT dacă una dintre coloanele tabelului cu o restricţie NOT NULL (obligatorii)
lipseşte din definirea vizualizării;
• restricţii referenţiale. Nu se poate insera sau actualiza valoarea unei chei externe decât dacă
există deja rândul părinte corespondent care conţine valoarea cheii în coloana cheii
primare. În sens invers, nu se poate şterge un rând părinte dacă există rânduri subordonate
care referă valoarea din rândul părinte, decât dacă restricţia a fost definită cu opţiunea
ON DELETE CASCADE. În general inserările în tabele trebuie făcute ierarhic (mai intai
rândurile părinte, apoi rândurile copii), iar ştergerile trebuie făcute în ordine inversă
(copiii înaintea părinţilor);
• restrictii de verificare (CHECK). O instructiune INSERT sau UPDATE nu poate stoca într-o
coloană o valoare care încalcă o restricţie CHECK definită pentru coloana respectivă.

78
Actualizarea datelor se referă la adăugarea unor noi rânduri într-o tabelă (cu instrucţiunea INSERT), la
modificarea valorilor uneia sau mai multor valori dintr-un rând (cu comanda UPDATE) şi la ştergerea
unui rând dintr-o tabelă (cu comanda DELETE).

6.1. Interogarea datelor (Comanda SELECT)

Comanda fundamentală a standardului SQL este SELECT, aceasta permiţând interogarea unei baze de
date.

Componentele interogării se numesc clause.

Sintaxa generală a comenzii SELECT este următoarea:


SELECT [ALL/DISTINCT/UNIQUE] listă de selecţie
FROM listă de relaţii (tabele)
WHERE condiţie de căutare asupra liniilor
GROUP BY listă de atribute care permit partiţionarea
HAVING condiţie asupra partiţiilor
ORDER BY listă de atribute;

Clauzele SELECT şi FROM sunt obligatorii. SELECT specifică datele care se selectează, iar clauza
FROM specifică relaţiile din care se selectează. Restul clauzelor sunt opţionale.

Exemplul 1: Să se selecteze toate produsele împreună cu toate datele acestora existente în baza de
date.
SELECT * FROM PRODUSE;

Exemplul 2: Să se selecteze toate produsele care expiră în data de 2016-07-03.


SELECT * FROM PRODUSE
WHERE data_expirarii=’2016-07-03’;

Interogarea datelor folosind operatorii IS şi IS NOT


Exemplu: Să se selecteze numele tuturor clienților care au completată adresa, apoi să se afişeze
numele tuturor persoanelor care nu au numărul de telefon completat.
SELECT nume_client FROM CLIENTI
WHERE adresa_client IS NOT NULL;

SELECT nume_client FROM CLIENTI


WHERE telefon_client IS NULL;

Interogarea datelor folosind operatorii logici AND, OR, NOT


Sintaxa pentru interogarea care utilizează un operator logic este:
condiţie1 AND condiţie 2;
condiţie1 OR condiţie 2;
NOT condiţie;

Exemplu: Să se determine numărul facturii şi codul produselor pentru produsele cu o cantitate mai
mare de 300 şi cu un preţ unitar mai mare sau egal ca 100.
SELECT cod_produs, nr_factura FROM LINII_FACTURI
WHERE cantitate>’300’ AND pret_unitar>=’100’;

79
Interogarea datelor folosind operatorul IN
SELECT valoare_câmp IN (valoare1, valoare2,...);

Această sintaxă a operatorului IN este similară cu următoarea listă de disjuncţii:


Valoare_câmp=valoare1 OR valoare_câmp=valoare2 OR ...;

Exemplu: Să se selecteze numărul facturii și codul produselor pentru produsele cu prețul unitar de 70,
80, 90.
SELECT * FROM LINII_FACTURI
WHERE pret_unitar IN (70.00,80.00,90.00);

Interogarea datelor folosind DISTINCT


Pentru a selecta seturi de valori distincte, adică eliminarea valorilor duplicat, în SQL se foloseşte
sintaxa DISTINCT, micşorând astfel setul de date.

Sintaxa acestei comenzi este:


SELECT DISTINCT nume_câmp1, nume_câmp2,...
FROM nume_tabela
WHERE comenzi;
sau
SELECT DISTINCT *
FROM nume_tabela;

Sintaxa DISTINCT se referă la o înregistrare care poate cuprinde unul sau mai multe câmpuri.

Exemplu: Să se afişeze toate datele distincte despre produse.


SELECT DISTINCT denumire_produs FROM PRODUSE;

Interogarea datelor folosind operatorul LIKE


Se cunosc mai multe modalităţi de utilizare a expresiei LIKE, şi anume:
• pentru o expresie care începe cu o anumită literă, de exemplu litera ‘A’: LIKE ‘A%’;
• pentru o expresie care se termină cu o anumită literă, de exemplu litera ‘A’: LIKE ‘%A’;
• pentru o expresie care include o anumită literă, de exemplu litera ‘A’: LIKE ‘%A%’;

Exemplu: Să se selecteze numele adresa şi emailul tuturor persoanelor din București care au adresă de
email pe gmail sau personal.
SELECT nume_client, adresa_client, email_client
FROM CLIENTI
WHERE adresa_client LIKE ‘%BUCUREȘTI %’ AND
(email_client LIKE ‘%gmail%’ OR email_client LIKE’%personal%’);

Interogarea datelor folosind operatorul BETWEEN


Operatorul se utilizează în combinaţie cu două valori între care se află valoarea la care se referă
operatorul.

Sintaxa este:
val BETWEEN minim AND maxim;
sau
val>=min AND val<=max;

80
Cele trei expresii val, min, max pot fi de tip numeric (numeric, decimal, int, smalint etc.) sau de tip
dată calendaristică.

Exemplu: Să se selecteze codurile tuturor facturilor înregistrate în perioada 1 ianuarie 2017 şi 1 mai
2017.
SELECT nr_factura
FROM FACTURI
WHERE data_factura BETWEEN ‘2017-01-01’ AND ‘2017-05-01’;

Interogarea datelor folosind funcţiile calendaristice YEAR, DAY, MONTH


Funcţiile YEAR, DAY, MONTH reţin dintr-un câmp de tip dată calendaristică anul, ziua, respectiv luna.

Exemplu: Să se vizualizeze codurile tuturor facturilor înregistrate în luna mai.


SELECT nr_factura
FROM FACTURI
WHERE MONTH(data_factura)=05;

Interogarea datelor folosind ordonarea


Datele se pot ordona după orice câmp. Ordonarea se poate face atât crescător cât şi descrescător.

Sintaxa pentru interogarea


• ordonată crescător este:
ORDER BY nume_câmp (ASC);
• ordonată descrescător este:
ORDER BY nume_câmp (DESC);

Dacă ORDER BY nu este urmat de ASC sau DESC, ordonarea se face implicit crescător.

Exemplu: Să se vizualizeze lista clienților în ordine alfabetică.


SELECT nume_client
FROM CLIENTI
ORDER BY nume_client;

Interogarea datelor din mai multe tabele


Atunci când este necesară obţinerea de informaţii din mai multe tabele se utilizează condiţii de join. În
acest fel liniile dintr-un tabel pot fi puse în legătură cu cele din alt tabel conform valorilor comune ale
unor coloane. Interogarea datelor din mai multe relaţii este strâns legată de noţiunea de cheie primară,
cheie secundară, restricţii de integritate, asocieri între relaţii.

Exemplu: Să se afişeze facturile, numele și adresa clienților corespunzătoare facturilor.


SELECT FACTURI.nr_factura, CLIENTI.nume_client,
CLIENTI.adresa_client
FROM FACTURI, CLIENTI
WHERE FACTURI.nume_client = CLIENTI.nume_client;

Observaţii:
• Atunci când în clauza FROM a unei comenzi SELECT apar mai multe tabele se realizează
produsul cartezian al acestora. De aceea numărul de linii rezultat creşte considerabil, fiind
necesară restricţionarea acestora cu o clauza WHERE. Se utilizează sintaxa:
nume_tabel.nume_câmp
Clauza FROM specifică două relaţii (FACTURI și CLIENTI).
81
Clauza SELECT cuprinde valori din relaţia FACTURI şi din relaţia CLIENTI, prin
urmare trebuie definite câmpurile în funcţie de tabela din care face parte.
• Clauza WHERE include condiţii care exprimă o egalitate între valorile identificatorului
nume_câmp a relaţiei nume_tabel şi a celei ale referinţei la acest identificator în tabela
referită.

Tipuri de asocieri pentru relaţii


Rolul unei relaţii este de a modela entităţi, între relaţii există aceleaşi tipuri de asocieri ca şi între
entităţi, şi anume asocieri una-la-una, una-la-multe, multe-la-multe.

• Asocieri de la una-la-una
Două relaţii stochează informaţii în asocierea una-la-una dacă unei înregistrări din relaţia A îi
corespunde (sau nu) o singură înregistrare din B.
Acest tip de asociere este utilizată mai rar. Există, totuşi, cazuri în care este necesară şi utilă stabilirea
unei astfel de relaţii.

Exemplu:

LINII_FACTURI PRODUSE

nr_factura# cod_produs#
cod_produs# denumire_produs
cantitate unitate_de_masura
pret_unitar data_expirarii

Asociere de tip 1:1

• Asocieri de la una-la-multe
O relaţie A se află într-o asociere de una-la-multe cu o relaţie B dacă fiecărei înregistrări din A îi
corespund una sau mai multe înregistrări din relaţia B. Unei înregistrări din relaţia B nu îi corespunde
decât maxim o înregistrare din relaţia A.

Sunt utilizate următoarele denumiri:


• B este relaţia copil, sau relaţia care referă la A, sau relaţia cheie străină;
• A este relaţia părinte (master), sau relaţia referită, sau relaţia cheie primară.

Exemplu:
FACTURI LINII_FACTURI

nr_factura# nr_factura#
data_factura cod_produs#
nume_client cantitate
delegat pret_unitar

Asociere de tip 1:M

Observaţie: Relaţia A are cheia primară „ nr_factura”, iar relaţia B are atributul „ cod_produs” cheie
externă.

82
• Asocieri de la mai multe-la-multe
O relaţie A se află în asociere de tipul multe-la-multe cu o relaţie B dacă unei înregistrări din relaţia A
îi pot corespunde mai multe înregistrări din relaţia B şi unei înregistrări din relaţia B îi pot corespunde
mai multe înregistrări din relaţia A.

O asociere N la M nu se defineşte direct, asocierea construindu-se cu ajutorul unei relaţii de joncţiune.


În această relaţie se păstrează legătura între cele două relaţii, precum şi informaţiile necesare.

Exemplu:
A 1:M B 1:M C
FACTURI LINII_FACTURI PRODUSE

nr_factura# nr_factura# cod_produs#


data_factura cod_produs# denumire_produs
nume_client cantitate unitate_de_masura
delegat pret_unitar data_expirarii

Asociere de tip N:M

Observaţie: În exemplul de mai sus, relaţia LINII_FACTURI realizează joncţiunea între relaţiile
FACTURI şi PRODUSE, stocând informaţiile privind nr_factura, cod_produs, cantitate, pret_unitar
etc.

Astfel, asocierea N la M este vizualizată sub forma a două relaţii de 1 la M.

Interogarea datelor din mai multe relaţii folosind aliasuri (sau pseudonime)
Un alias este o redenumire fie a unui câmp, fie a unei relaţii. Aliasurile sunt utilizate la eliminarea
rescrierii complete a denumirii unei relaţii sau a unui câmp, redenumindu-le într-un mod simplificat.

Sintaxa utilizată este:


nume_relaţie/camp AS nume_nou;
sau
nume_relaţie/camp nume_nou;

Există posibilitatea de a utiliza aliasuri pentru tabelele din clauza FROM şi utilizarea lor în cadrul
comenzii SELECT respective (alias.coloana). Această identificare (prin 'tabel.coloana' sau
'alias.coloana') este obligatorie atunci când se face referinţă la o coloană ce apare în mai mult de un
tabel din clauza FROM.

Exemplu: Să se afişeze facturile, numele și adresa clienților corespunzătoare facturilor, folosind


aliasuri.
SELECT F.nr_factura, C.nume_client, C.adresa_client
FROM FACTURI F, CLIENTI C
WHERE F.nume_client = C.nume_client;

Observaţie: În cazul în care un atribut apare doar într-o relaţie dintre cele menţionate în listă, nu este
obligatorie precizarea relaţiei (adică a aliasului) din care face parte atributul respectiv.

Interogarea datelor din mai multe relaţii folosind tipuri de asocieri


Tipurile de asocieri utilizate în interogarea mai multor relaţii sunt:
83
1) INNER JOIN (joncţiunea internă);
2) LEFT OUTER JOIN (semijoncţiunea la stânga);
3) RIGHT OUTER JOIN (semijoncţiunea la dreapta);

1) INNER JOIN (joncţiunea internă). Sintaxa


SELECT ...FROM tabel_A INNER JOIN tabel_B (condiţii de join)
selectează toate informaţiile din relaţiile A şi B care corespund condiţiilor de asociere.

Exemplu: Selectaţi numărul facturii şi denumirea produselor fiecărei facturi folosind operaţia de join,
apoi utilizând clauza WHERE.
SELECT F.nr_factura, P.cod_produs, P.denumire_produs
FROM LINII_FACTURI F INNER JOIN PRODUSE P
ON (F.cod_produs =P.cod_produs);

SELECT F.nr_factura, P.denumire_produs


FROM LINII_FACTURI F, PRODUSE P
WHERE F.cod_produs =P.cod_produs;

Observaţii:
• Rezultatul este acelaşi. Valorile NULL vor fi ignorate;
• Sintaxei SELECT-FROM-INNER JOIN i se pot adăuga şi alte condiţii, neincluse în condiţiile
de join, dacă acestea se referă la alte câmpuri decât cele care participă la join.

2) LEFT OUTER JOIN (semijoncţiunea la stânga). Sintaxa


SELECT ...FROM tabel_A LEFT OUTER JOIN tabel_B
ON (condiţii de join)

selectează toate informaţiile din A, pe care le completează cu informaţii din B, în măsura în care
satisfac condiţiile de join; acolo unde nu vor exista informaţii din B, acestea vor fi completate cu
NULL.

Exemplu: Selectaţi toate facturile. Dacă există informaţii despre aceste facturi, afişaţi şi aceste
informaţii.
SELECT *
FROM LINII_FACTURI F LEFT OUTER JOIN PRODUSE P
ON (F.cod_produs =P.cod_produs);

Observaţie: Ordinea în care se scrie denumirea relaţiei în sintaxa LEFT OUTER JOIN este foarte
importantă. Astfel, relaţia din stânga este relaţia primară, adică relaţia pentru care se doreşte returnarea
tuturor informaţiilor; relaţia din dreapta este relaţia secundară, adică informaţiile din ea sunt necesare
doar în măsura în care se potrivesc condiţiilor de asociere. Astfel se explică şi denumirea de asociere
de la stânga spre exterior.

3) RIGHT OUTER JOIN (semijoncţiunea la dreapta). Sintaxa


SELECT ...FROM tabel_A RIGHT OUTER JOIN tabel_B
ON (condiţii de join)

selectează toate informaţiile din B, pe care le completează cu informaţii din A, în măsura în care
satisfac condiţiile de join; acolo unde nu vor exista informaţii din A, acestea vor fi completate cu
NULL.

84
Exemplu: Selectaţi toate facturile și produsele corespunzătoare, inclusiv facturile fără niciun produs.
SELECT *
FROM PRODUSE P RIGHT OUTER JOIN LINII_FACTURI F
ON (P.cod_produs =F.cod_produs);

Observaţie: Sintaxa RIGHT OUTER JOIN este utilizată mai rar; de obicei se utilizează sintaxa LEFT
OUTER JOIN.

Interogarea datelor din mai multe relaţii folosind instrucţiunea UNION


Sintaxa interogării datelor din mai multe relaţii folosind instrucţiunea UNION este:
SELECT Câmp 1, Câmp 2, ..., Câmp n
FROM Tabel 1
UNION (ALL)
SELECT Câmp 1A, Câmp 2A,..., Câmp nA
FROM Tabel 2

şi returnează înregistrări distincte, dacă este folosită instrucţiunea UNION şi toate înregistrările, dacă
se foloseşte UNION ALL. Astfel operatorul UNION elimină duplicatele, iar UNION ALL
vizualizează toate înregistrările, inclusiv duplicatele.

Pentru a utiliza această interogare, trebuie să se ţină seama de două cerinţe: domeniile Câmp 1A, Câmp
2A,..., Câmp nA şi Câmp 1, Câmp 2, ..., Câmp n trebuie să fie respectiv aceleaşi şi, numărul de
câmpuri din fiecare interogare trebuie să coincidă.

Operatorul UNION se foloseşte atunci când între relaţii nu există o asociere directă.

Interogarea datelor mai multor relaţii folosind operatorul de concatenare a două şiruri de caractere
Rolul operatorului de concatenare a două şiruri de caractere este de a uni două şiruri de caractere într-
unul singur. Este utilizat în toate SGBD-urile, cu mici modificări ale simbolului: în SQL se foloseşte
simbolul ‚+’, în Oracle simbolul ‚||’ etc.

Se pot concatena o constantă cu un câmp, sau două câmpuri. Câmpurile trebuie să fie de tip text.

Sintaxa pentru concatenarea a două câmpuri este


CONCAT(Câmp1, Câmp2)

sau inserând virgula, spaţiu sau oricare marcaj de delimitare


CONCAT(Câmp1,’,’, Câmp2)
sau
CONCAT(Câmp1,’ ’, Câmp2)
concatenează cele două constante într-una singură ’ Câmp1 Câmp2’.

Observaţie: Concatenarea prezintă dezavantajul afişării câmpurilor NULL.

Interogarea datelor folosind funcţiile totalizatoare:


• MAX
• MIN
• COUNT
• SUM
• AVG

85
• Interogarea datelor folosind funcţia MAX
Sintaxa:
SELECT MAX(Nume_câmp) FROM Nume_tabela

returnează un număr egal cu valoarea maximă a câmpului Nume_câmp din relaţia Nume_tabela,
valorile NULL fiind ignorate.

Exemplu: Selectaţi cea mai recentă factură din tabela FACTURI, fără a da un nume rezultatului, apoi
cu nume pentru câmpul rezultat.
SELECT MAX(data_factura) FROM FACTURI;

SELECT MAX(data_factura) AS data_ultimei_înregistrari


FROM FACTURI;

• Interogarea datelor folosind funcţia MIN


Funcţia MIN este o funcţie similară cu funcţia MAX, cu ajutorul căreia se poate determina valoarea
cea mai mică dintr-un câmp.

Atât funcţia MIN cât şi funcţia MAX se pot aplica doar pentru tipurile de date numeric sau dată
calendaristică.

• Interogarea datelor folosind funcţia COUNT


Sintaxa:
• SELECT COUNT (*) FROM Nume_tabela - returnează un număr egal cu numărul de
înregistrări ale tabelei Nume_tabela;
• SELECT COUNT (Nume_câmp) FROM Nume_tabela - returnează un număr egal cu
numărul de valori nenule ale câmpului Nume_câmp din tabela Nume_tabela. Sunt
ignorate valorile NULL;
• SELECT COUNT(DISTINCT Nume_câmp) FROM Nume_tabela - returnează un
număr egal cu numărul de valori distincte nenule ale câmpului Nume_câmp din tabela
Nume_tabela. Sunt ignorate valorile NULL.

Exemplu: Precizaţi numărul de produse.


SELECT COUNT(cod_produs) AS nr_produselor FROM PRODUSE;

• Interogarea datelor folosind funcţia SUM


Sintaxa:
• SELECT SUM (Nume_câmp) FROM Nume_tabela - returnează un număr egal cu
suma tuturor valorilor câmpului Nume_câmp din relaţia Nume_Tabela. Sunt ignorate
valorile NULL;
• SUM (DISTINCT Nume_câmp) FROM Nume_tabela - returnează un număr egal cu
suma valorilor distincte ale câmpului Nume_câmp din relaţia Nume_Tabela.

Funcţia SUM se aplică acelor câmpuri care au domeniul de valori de tipul FLOAT, DECIMAL,
NUMERIC, INT etc. şi nu are sens pentru câmpuri de tip text.

Exemplu: Precizaţi suma tuturor încasărilor existente pe facturile emise.


SELECT SUM(DISTINCT total_valoare_factura) FROM FACTURI;

86
• Interogarea datelor folosind funcţia AVG
Sintaxa:
AVG (nume_câmp) FROM Nume_tabela
returnează un număr egal cu media aritmetică a tuturor valorilor câmpului Nume_câmp din relaţia
Nume_tabela. Valorile NULL sunt ignorate.

Funcţia AVG se utilizează doar pentru date de tip numeric: INT, FLOAT, NUMERIC.

Exemplu: Selectaţi media preturilor produselor.


SELECT AVG (pret_unitar) FROM LINII_FACTURI;

Interogarea datelor folosind instrucţiunea GROUP BY


Prin instrucţiunea GROUP BY se grupează datele după fiecare produs în parte.

Exemplu: Selectaţi fiecare produs în parte grupându-le crescător şi precizaţi cantitatea vândută din
fiecare tip.
SELECT P.denumire_produs, SUM(F.cantitate) AS suma
FROM PRODUSE P, LINII_FACTURI F
WHERE F.cod_produs= P.cod_produs
GROUP BY P.cod_produs;

Observație: Menţionarea clauzelor SELECT, FROM, WHERE, GROUP BY, ORDER BY în această
ordine este obligatorie. Greşeala frecventă care duce la apariţia unor mesaje de eroare este aceea a
introducerii unor câmpuri după care se grupează în clauza SELECT şi neintroducerea lor în clauza
GROUP BY.

Funcţiile de agregare se pot folosi ca extensii ale clauzei GROUP BY:


• ROLLUP - permite instrucţiunii SELECT să calculeze niveluri de subtotal multiple peste un
grup de dimensiuni;
• CUBE - generează toate subtotalurile care pot fi calculate dintr-un CUBE pe dimensiunile
specificate;
• GROUPING;
• GROUPING SET.

Interogarea datelor folosind instrucţiunea HAVING


Instrucţiunea HAVING se utilizează numai în combinaţie cu instrucţiunea GROUP BY. Clauza
HAVING este utilizată când se doreşte filtrarea datelor grupate conform unor criterii. Aceste criterii
presupun compararea unor valori obţinute prin apelarea unor funcţii totalizatoare. Aceste tipuri de
comparări presupun gruparea datelor. Din această cauză, HAVING cere obligatoriu clauza GROUP
BY.

Exemplu: Selectaţi produsele grupate după cod care au preţul unitar cuprins între 500 şi 3000.
SELECT P.denumire_produs, P.cod_produs, F.pret_unitar
FROM PRODUSE P, LINII_FACTURI F
WHERE F.cod_produs= P.cod_produs
GROUP BY P.cod_produs
HAVING F.PRET_UNIRAR BETWEEN 500 AND 3000;

87
6.2. Adăugarea de noi tupluri (Comanda INSERT)

În vederea adăugării unor rânduri noi într-o tabelă sau într-o vizualizare se utilizează comanda
INSERT. Instrucţiunea are două forme de bază: una în care valorile coloanelor sunt specificate chiar în
instrucţiune şi alta în care valorile sunt selectate dintr-un tabel sau o vizualizare, folosind o
subinterogare.

Inserarea unui singur rând de date folosind clauza Values


Instrucţiunea INSERT care foloseşte o clauză VALUES poate crea un singur rând la fiecare rulare,
deoarece valorile pentru rândul de date respectiv sunt specificate chiar în instrucţiune.

Sintaxa generală a instrucţiunii este următoarea:


INSERT INTO nume_tabel_sau_vizualizare[(lista_de_coloane)]
VALUES (lista_de_valori);
sau
INSERT INTO nume_tabel/nume_view [(col1[, col2[,…]])]
VALUES (expresia1[, expresia2[,…]]) / subcerere;
• expresia1, expresia2, reprezintă expresii a căror evaluare este atribuită coloanelor precizate (se
inserează o linie);
• subcerere, reprezintă o interogare (se inserează una sau mai multe linii).

Exemplu:
INSERT INTO PRODUSE (cod_produs, denumire_produs,
unitate_de_masura, data_expirarii)
VALUES (50, ‘PAINE’, ‘KG’,’2017-05-15’);

Clauza VALUES specifică valorile ce vor fi introduse în tabel sau vizualizare. Pentru a insera mai
multe linii prin aceeaşi instrucţiune INSERT, în locul acestei clauze se va preciza o subcerere.
Dacă nu se mai cunoaşte ordinea de declarare a coloanelor se foloseşte comanda DESCRIBE care va
afişa lista coloanelor definite pentru tabela respectivă, tipul şi lungimea lor.

Se pot remarcă următoarele:


• lista de coloane este opţională, dar dacă este inclusă trebuie să fie încadrată între paranteze
rotunde;
• dacă lista de coloane este omisă, trebuie specificată o valoare pentru fiecare coloană din tabel,
în ordinea în care sunt definite coloanele în tabel. Este bine ca întotdeauna să se includă
lista de coloane, deoarece omiterea acesteia face ca instrucţiunea INSERT să fie
dependentă de definiţia tabelului. Dacă o coloană este modificată sau în tabel este
adăugată o nouă coloană, chiar şi opţională, probabil instrucţiunea INSERT va eşua la
următoarea rulare;
• dacă lista de coloane este specificată, lista de valori trebuie să conţină o valoare pentru fiecare
coloană din listă, în aceeaşi ordine. Cu alte cuvinte, între lista de coloane şi lista de valori
trebuie să existe o corespondenţă una-la-una. Orice coloană care lipseşte din listă va primi
o valoare nulă, presupunund că valorile nule sunt acceptate în coloana respectivă;
• în clauza VALUES, valorile de tip caracter şi dată calendaristică trebuie incluse între
apostrofuri. Nu se recomandă includerea între apostrofuri a valorilor numerice, întrucât
aceasta ar determina conversii implicite la tipul NUMBER;
• pentru introducerea de valori speciale în tabel, pot fi utilizate funcţii;
• adăugarea unei linii care va conţine valori NULL se poate realiza în mod implicit, prin
omiterea numelui coloanei din lista de coloane. Exemplu:

88
INSERT INTO PRODUSE (cod_produs, denumire_produs,
unitate_de_masura)
VALUES (50, ‘PAINE’, ‘KG’);
sau explicit, prin specificarea în lista de valori a cuvântului cheie NULL sau a şirului vid (‘’)
în cazul şirurilor de caractere sau datelor calendaristice. Exemplu:
INSERT INTO PRODUSE (cod_produs, denumire_produs,
unitate_de_masura, data_expirarii)
VALUES (50, ‘PAINE’, ‘KG’, NULL);

Inserări masive folosind instrucţiunea SELECT internă


Aşa cum se observă, este nevoie de foarte mult cod pentru a insera în tabel un singur rând de date
folosind o instrucţiune INSERT cu clauza VALUES.

O altă soluţie, care poate fi folosită pentru a insera rânduri multiple într-un tabel, este forma care
foloseşte o instrucţiune SELECT internă. Această formă este utilă şi pentru stabilirea următoarei valori
disponibile pentru o cheie primară cu valori secvenţiale. De asemenea, poate fi folosită atunci când se
creează un tabel temporar pentru testare, care va fi populat cu toate datele dintr-un alt tabel.

Sintaxa generală a instrucţiunii este:


INSERT INTO nume_tabel_sau_vizualizare[(lista_de_coloane)]
SELECT instructiune_select;

Se remarcă următoarele:
• lista de coloane este opţională, dar dacă este inclusă trebuie să fie încadrată între paranteze
rotunde;
• dacă lista de coloane este omisă, instrucţiunea SELECT internă trebuie să furnizeze o valoare
pentru fiecare coloană din tabel, în ordinea în care sunt definite coloanele în tabel. Este
bine ca întotdeauna să se includă lista de coloane, deoarece omiterea acesteia face ca
instrucţiunea INSERT să fie dependentă de definiţia tabelului. Dacă o coloană este
modificată sau în tabel este adăugată o nouă coloană, chiar şi opţională, probabil
instrucţiunea INSERT va eşua la următoarea rulare;
• dacă lista de coloane este specificată, instrucţiunea SELECT internă trebuie să furnizeze o
valoare pentru fiecare coloană din lista de valori, în aceeaşi ordine. Cu alte cuvinte, între
lista de coloane şi setul de rezultate al instrucţiunii SELECT trebuie să existe o
corespondenţă ună-la-ună. Orice coloană care lipseşte din listă va primi o valoare nulă,
presupunând că valorile nule sunt acceptate în coloana respectivă;
• cuvântul cheie NULL poate fi folosit în instrucţiunea SELECT pentru specificarea unei valori
nule pentru o coloană.

Inserarea unor valori specifice de tip dată calendaristică


Formatul DD-MON-YY este de obicei folosit pentru a insera o valoare de tip dată calendaristică. Cu
acest format, secolul este implicit cel curent. Deoarece data conţine de asemenea informaţii despre
timp, ora implicită este 00:00:00.

Dacă o dată calendaristică necesită specificarea altui secol sau oră, trebuie folosită funcţia TO_DATE.

Exemplu:
INSERT INTO PRODUSE (cod_produs, denumire_produs,
unitate_de_masura, data_expirarii)
VALUES (50, ‘PAINE’, ‘KG’, TO_DATE(‘FEB-3 , 2017’, 'MON-DD,
YYYY’));
89
Inserare de valori folosind variabile de substituţie

• Crearea unui script interactiv prin folosirea variabilelor de substituţie SQL*Plus


Exemplu:
INSERT INTO PRODUSE (cod_produs, denumire_produs, unitate_de_masura)
VALUES (‘&cod_produs’, ‘&denumire_produs’, ‘&unitate_de_masura’)

• Crearea unui script pentru manipularea datelor


Comanda împreună cu variabilele de substituţie pot fi salvate într-un fişier şi acesta poate fi executat.
De fiecare dată când se execută fişierul, sunt cerute valori noi pentru variabile. Prin intermediul
comenzii SQL*Plus ACCEPT, mesajele afișate la cererea introducerii valorilor pot fi modificate.
• ACCEPT - memoreaza valoarea într-o variabilă;
• PROMPT - afişează textul specificat.

Exemplu:
ACCEPT cod_produs PROMPT 'Introduceti codul produsului:'
ACCEPT denumire_produs PROMPT 'Introduceti denumirea produsului:'
INSERT INTO PRODUSE (cod_produs, denumire_produs)
VALUES (‘&cod_produs’, ‘&denumire_produs’)

Exemplul înregistrează informaţia pentru un produs, în tabelul PRODUSE. Utilizatorului îi sunt cerute
codul produsului şi denumirea produsului, folosind mesajele de prompt stabilite în ACCEPT.

Parametrul de substitutie SQL*Plus nu trebuie precedat de & când este referit într-o comanda
ACCEPT. Pentru a continua o comanda SQL*PLUS pe linia următoare se foloseşte o linie (-).

Copierea înregistrarilor dintr-un alt tabel


Comanda INSERT poate fi folosită pentru a adăuga înregistrări într-un tabel, valorile pentru câmpuri
fiind extrase dintr-un tabel existent. Pentru aceasta se foloseşte, în locul clauzei VALUES, o
subinterogare (Nu se mai folosește clauza VALUES).

Sintaxa este urmtoărea:


INSERT INTO nume_tabel [ coloana ( , coloana ) ]
Subinterogare;

Numărul şi tipul câmpurilor (coloanelor) din lista specificată în comanda INSERT trebuie să
corespundă numărului şi tipului valorilor din subinterogare.

Exemplu:
INSERT INTO PRODUSE (cod_produs, denumire_produs,
unitate_de_masura)
SELECT cod_produs, denumire_produs, unitate_de_masura
FROM PRODUSE
WHERE denumire_produs = 'PAINE';

Inserări multitabel
O inserare multitabel presupune introducerea de linii calculate pe baza rezultatelor unei subcereri, în
unul sau mai multe tabele. Acest tip de inserare, introdus de Oracle, este util în mediul data
warehouse. Astfel, datele extrase dintr-un sistem sursă, pot fi transformate utilizând instrucţiuni
INSERT multitabel, spre a fi încărcate în obiectele bazei de date.
90
Sintaxa clauzei inserare_multi_tabel este următoarea:
INSERT ALL INTO…[VALUES…] [INTO…[VALUES…] …]
| inserare_condiţionată| subcerere

Clauza inserare_condiţionată are forma următoare:


[ALL | FIRST]
WHEN condiţie THEN INTO…[VALUES…]
[INTO…[VALUES…] …]
[WHEN condiţie THEN INTO…[VALUES…]
[INTO…[VALUES…] …] …]
[ELSE INTO…[VALUES…]
[INTO…[VALUES…] …] …]

Pentru a efectua o inserare multitabel necondiţionată, sistemul va executa câte o instrucţiune


INSERT…INTO pentru fiecare linie returnată de subcerere.

Utilizând clauza inserare_condiţionată, decizia inserării unei linii depinde de condiţia specificată prin
intermediul opţiunii WHEN. Expresiile prezente în aceste condiţii trebuie să facă referinţă la coloane
returnate de subcerere. O instrucţiune de inserare multitabel poate conţine maxim 127 clauze WHEN.

Specificarea opţiunii ALL determină evaluarea tuturor condiţiilor din clauzele WHEN. Pentru cele a
căror valoare este TRUE, se inserează înregistrarea specificată în opţiunea INTO corespunzătoare.

Opţiunea FIRST determină inserarea corespunzătoare primei clauze WHEN a cărei condiţie este
evaluată TRUE. Toate celelalte clauze WHEN sunt ignorate.

Dacă nici o condiţie din clauzele WHEN nu este TRUE, atunci sistemul execută clauza INTO
corespunzătoare opţiunii ELSE, iar dacă aceasta nu există, nu efectuează nici o acţiune.

Inserările multitabel pot fi efectuate numai asupra tabelelor, nu şi asupra vizualizărilor. De asemenea,
acest tip de inserare nu se poate efectua asupra tabelelor distante. Subcererea dintr-o instrucţiune
corespunzătoare unei inserări multitabel nu poate utiliza o secvenţă.

6.3. Modificarea tuplurilor din tabele (Comanda UPDATE)

În funcţie de momentul în care se doreşte realizarea modificărilor asupra bazei de date, utilizatorul
poate folosi una din următoarele comenzi:
• SET AUTOCOMMIT IMM[EDIATE] (schimbările se efectuează imediat);
• SET AUTOCOMMIT OFF (schimbările sunt păstrate într-un buffer).

La execuţia comenzii COMMIT se permanentizează schimbările efectuate, iar la execuţia comenzii


ROLLBACK se renunţă la schimbările realizate.

Instrucţiunea UPDATE este folosită pentru actualizarea datelor din coloanele unui tabel (sau ale unei
vizualizări).

Valorile câmpurilor care trebuie modificate pot fi furnizate explicit sau pot fi obţinute în urma unei
cereri SQL.

Sintaxa generală a instrucţiunii UPDATE:


91
UPDATE nume_tabel_sau_vizualizare
SET nume_coloana = expresie[,nume_coloana = expresie...]
[ WHERE conditie];

Sau mai general, expresia poate fi o cerere:


UPDATE nume_tabel_sau_vizualizare
SET (column1[,column2[,…]]) = (subquery) / column = expr
/(query)
[WHERE condition]

Dacă este nevoie, se pot modifica mai multe înregistrări simultan.

Observaţii:
• Pentru a se putea executa instrucţiunea UPDATE, utilizatorul care o lansează în execuţie
trebuie să aibă acest privilegiu;
• Dacă nu este specificată clauza WHERE se vor modifica toate liniile;
• Cererea trebuie să furnizeze un număr de valori corespunzător numărului de coloane din
paranteza care precede caracterul de egalitate.

Se remarcă următoarele:
• clauza SET conţine o listă cu una sau mai multe coloane, împreună cu o expresie care
specifică noua valoare pentru fiecare coloană. Aceasta este o listă de perechi
(NUME,VALOARE), separate prin virgule, cu un operator de egalitate între fiecare
NUME şi VALOARE;
• expresia poate fi o constantă, un alt nume de coloană sau orice altă expresie pe care SQL o
poate transforma într-o singură valoare, care poate fi apoi atribuită coloanei respective;
• clauza WHERE conţine o expresie care limitează rândurile actualizate. Dacă această clauză
este omisă, motorul SQL va încerca să actualizeze toate rândurile din tabel sau din
vizualizare.

Oracle permite utilizarea valorii implicite DEFAULT în comenzile INSERT și UPDATE. Unei
coloane i se atribuie valoarea implicită definită la crearea sau modificarea structurii tabelului dacă:
• nu se precizează nici o valoare;
• dacă se precizează cuvântul cheie DEFAULT în comenzile INSERT sau UPDATE.

Dacă nu este definită nici o valoare implicită pentru coloana respectivă, sistemul îi atribuie valoarea
NULL. Cuvântul cheie DEFAULT nu poate fi specificat la actualizarea vizualizărilor.

Cazurile în care instrucţiunea UPDATE nu poate fi executată sunt similare celor în care eşuează
instrucţiunea INSERT.

Notă: În general, se folosește cheia primară pentru a identifica o singură înregistrare. Folosirea altor
coloane poate determina modificarea mai multor înregistrări.

De exemplu, identificarea unei singure înregistrări în tabelul PRODUSE prin denumire poate fi
periculoasă, deoarece pot exista mai multe produse cu aceeaşi denumire.

Comanda UPDATE modifică anumite înregistrări dacă este specificată clauza WHERE.

Exemplul următor mărește prețul unitar cu 25% pentru produsul cu codul 50.
UPDATE LINII_FACTURI
92
SET pret_unitar = pret_unitar * 1.25
WHERE cod_produs = 50;

Actualizarea înregistrarilor folosind subinterogări după mai multe câmpuri


Exemplul următor măreşte prețul unitar cu 15% pentru produsele care au prețul unitar identic cu cel al
produsului cu codul 50.
UPDATE LINII_FACTURI
SET pret_unitar = pret_unitar * 1.15
WHERE pret_unitar IN
(SELECT pret_unitar
FROM LINII_FACTURI
WHERE cod_produs = 50);

Modificarea înregistrarilor folosind subinterogări după mai multe câmpuri (folosind valori dintr-un
alt tabel)
În clauza SET a unei comenzi UPDATE pot fi implementate subinterogări după mai multe câmpuri.
UPDATE nume_tabel
SET (coloana, coloana, ...) =
(SELECT coloana, coloana,
FROM nume_tabel
WHERE condition)
WHERE condition;

Dacă se încercă atribuirea unei valori unui câmp care are legată de o constrângere de integritate, va
rezulta o eroare.

6.4. Ştergerea tuplurilor din tabele (Comanda DELETE)

Instrucţiunea DELETE şterge unul sau mai multe rânduri dintr-un tabel. Instrucţiunea poate să
folosească şi o vizualizare, dar numai dacă aceasta se bazează pe un singur tabel (ceea ce înseamnă că
instrucţiunile DELETE nu pot fi folosite pentru vizualizări care conţin uniuni). În instrucţiunile
DELETE nu sunt referite niciodată coloane, doarece instrucţiunea şterge rânduri întregi de date,
inclusiv toate valorile datelor (toate coloanele) din rândurile afectate. Dacă se şterge o singură valoare
din rândurile existente, se folosește instrucţiunea UPDATE pentru a înlocui valorile respective cu
valori nule (presupunând că valorile nule sunt permise în acele coloane).

Sintaxa generală a instrucţiunii DELETE este:


DELETE FROM nume_tabel_sau_vizualizare [AS alias]
[ WHERE conditie ];

Se remarcă următoarele:
• comanda DELETE nu şterge structura tabelului;
• clauza WHERE este opţională. Totuşi, este folosită aproape întotdeauna, deoarece o
instrucţiune DELETE fără o clauză WHERE încearcă să şteargă toate rândurile din tabel.
În clauza WHERE pot fi folosite şi subcereri;
• atunci când este inclusă, clauza WHERE specifică rândurile care urmează să fie şterse. Orice
rând pentru care condiţia WHERE este evaluată ca adevărată este şters din tabel;
• nu se pot şterge rânduri dacă se încalcă o restricţie referenţială. În general, rândurile
subordonate trebuie şterse înaintea rândurilor părinte;
• pentru a şterge linii identificate cu ajutorul valorilor din alte tabele, se utilizează subcereri;

93
• comanda nu poate fi folosită pentru ştergerea valorilor unui câmp individual. Acest lucru se
poate realiza cu ajutorul comenzii UPDATE.

Dacă se încearcă ştergerea unei înregistrări care conţine o valoare implicată într-o constrângere de
integritate, atunci va fi returnată o eroare.

În cazul în care constrângerea de integritate referenţială a fost definită utilizând opţiunea ON DELETE
CASCADE, atunci instrucţiunea DELETE va şterge atât liniile indicate, cât şi liniile „copil“ din
tabelele corespunzătoare.

Ştergerile accidentale pot fi omise, restaurându-se valorile iniţiale prin comanda AUTOCOMMIT
OFF.

Exemplu: Stergeţi toate produsele care expiră înainte de 1 Ianuarie, 2017. Ştergerile să nu fie efectuate
imediat ci ulterior.
SET AUTOCOMMIT OFF
DELETE FROM PRODUSE
WHERE data_expirarii > TO_DATE('01-01-17', 'DD-MM-YY');

Ştergerea înregistrărilor folosind valori dintr-un alt tabel


Pot fi folosite subinterogări pentru a şterge înregistrări dintr-un tabel, folosind informaţiile din altul.

Exemplul de mai jos şterge toate produsele cu denumirea PAINE. Subinterogarea caută în tabelul
PRODUSE codul produsului PAINE, apoi furnizează codul interogării principale, care şterge
înregistrările din LINII_PRODUSE pe baza acestuia.
DELETE FROM LINII_PRODUSE
WHERE cod_produs =
(SELECT cod_produs
FROM PRODUSE
WHERE denumire_produs = 'PAINE');

Încălcarea constrângerii de integritate


Dacă se încerarcă ştergerea unei înregistrări care conţine un câmp cu o valoare legată de o
constrângere de integritate, se obţine o eroare.

În exemplul de mai jos se încearcă ştergerea produsului cu codul 10 din tabelul PRODUSE, dar
aceasta provoacă o eroare, deoarece codul produsului este folosit ca şi cheie externă în tabelul
LINII_FACTURI.

Dacă înregistrarea părinte, care se încearcă să se şteargă, are înregistrări fii, atunci se primește un
mesaj de eroare: child record found violation ORA - 02292.
DELETE FROM PRODUSE
WHERE cod_produs = 50;

7. Limbajul de control al datelor (LCD)


Limbajul pentru controlul datelor (LCD – Data Control Language) include instrucţiuni SQL care
permit administratorilor să controleze accesul la datele din baza de date şi folosirea diferitelor
privilegii ale sistemului DBMS, cum ar fi privilegiul de oprire şi pornire a bazei de date.

94
Controlul unei baze de date cu ajutorul SQL-ului se referă la:
• asigurarea confidentialitaţii şi securităţii datelor;
• organizarea fizică a datelor;
• realizarea unor performanţe;
• reluarea unor acţiuni în cazul unei defecţiuni;
• garantarea coerenţei datelor în cazul prelucrării concurente.

Sistemul de gestiune trebuie:


• să pună la dispoziţia unui număr mare de utilizatori o mulţime coerentă de date;
• să garanteze coerenţa datelor în cazul manipulării simultane de către diferiţi utilizatori.

Comenzile de control la dispoziția administratorului (DBA) - GRANT, REVOKE – sunt utilizate pentru
a da sau a lua drepturi de acces (la comenzi LMD, deci la operarea unor modificări a bazei de date).

Sintaxa:
GRANT
[Privilegii]
ON
TO
Utilizator IDENTIFIED BY ’DenumireParola’;

REVOKE
[Privilegii]
ON
TO
Utilizator IDENTIFIED BY ’DenumireParola’;

7.1. Asigurarea confidențialității și securității datelor

Sistemul de gestiune trebuie să asigure securitatea fizică şi logică a informaţiei şi să garanteze că


numai utilizatorii autorizaţi pot efectua operaţii corecte asupra bazei de date. Pentru acestea, există
mecanisme care permit identificarea şi autentificarea utilizatorilor şi există proceduri de acces
autorizat care depind de date şi de utilizator, cum ar fi:
 conturi pentru utilizatori, cu parolă folosită pentru autentificare;
 grupuri, roluri, privilegiile și profilurile - acestea permit nu numai constrângeri ci și stabilirea
unei politici de securitate. Pentru a accesa un obiect, un utilizator trebuie să aibă privilegiile
necesare. Privilegiile pot fi acordate direct unui utilizator sau pot fi grupate în roluri, care la
rândul lor pot fi acordate utilizatorului.
Exemplu: Un forum de discuții are utilizatori grupați pe roluri ca: administrator, moderator,
membru. Fiecare rol poate avea privilegii diferite: administratorul poate configura baza de
date (modifică schema, adaugă tabele, configurează interfața); moderatorul poate valida,
modifica, șterge postările membrilor; membrii pot adăuga înregistrări.

7.2. Reluarea unor acțiuni în cazul unei defecțiuni

Reluarea unor acțiuni în cazul apariţiei unei defecțiuni hard sau soft presupune recuperarea ultimei
stări coerente a bazei de date. În funcţie de defecţiunea care a determinat întreruperea lucrului,
restaurarea bazei de date se realizează automat de SGBD sau manual, adică necesită intervenţie
umană.

95
Salvarea bazei de date se realizează conform unei strategii existând combinaţiile:
 copii ale bazei de date şi copii ale jurnalelor acestora;
 jurnale ale tranzacţiilor;
 jurnale ale imaginii înregistrărilor din baza de date.

Copiile bazei de date - pot fi realizate automat de sistem la anumite intervale de timp sau la comanda
administratorului bazei de date, ori de câte ori este nevoie şi de obicei pe un alt suport magnetic decât
cele pe care rezidă baza de date. Aceste copii pot fi utilizate doar în situaţia în care prelucrările
efectuate între momentul realizării copiilor şi cel al apariţiei unei defecţiuni pot fi reluate. Acest lucru
este posibil doar dacă prelucrările sunt efectuate într-o secvenţă cunoscută iar timpul necesar pentru
reprocesarea nu este foarte mare. Durata mare de execuţie pentru astfel de copii face ca anumite
SGBD-uri să recurgă la copii ale jurnalelor bazei de date. Volumul datelor care vor fi copiate în acest
caz va fi mai mic, iar procesul de restaurare va implica într-o măsură mai mică intervenţia umană.

Jurnalul tranzacţiilor - este un fişier special întreţinut de SGBD, în care sunt memorate informaţiile
despre tranzacţiile efectuate asupra bazei de date cum sunt:
 identificatorul sau codul tranzacţiei;
 momentul începerii execuţiei tranzacţiei;
 numărul terminalului sau identificatorul utilizatorului care a iniţiat tranzacţia;
 datele introduse;
 înregistrările modificate şi tipul modificării.

Jurnalul imaginilor înregistrărilor din baza de date - se deosebeşte de jurnalul tranzacţiilor prin aceea
că el nu conţine descrierea operaţiilor efectuate asupra bazei de date, ci efectul acestora.

7.3. Garantarea coerenței datelor în cazul prelucrării concurente

Sistemul de gestiune a bazelor de date asigură accesul concurent al mai multor utilizatori la baza de
date. Fiecare utilizator trebuie să aibă o vedere validă și consistentă asupra bazei de date, incluzând și
modificările făcute de alți utilizatori; în același timp, procesarea incorectă a datelor trebuie evitată,
pentru a nu afecta consistența datelor sau integritatea acestora.

În funcție de complexitatea operației de acces concurent, problema gestionării concurenței se


complică:
 acces concurent a mai multor utilizatori numai pentru consultarea datelor;
 acces concurent a mai multor utilizatori cu unul dintre ei modificând datele;
 acces concurent a mai multor utilizatori cu mai mulți dintre ei modificând datele.

Pentru ultimele două, se utilizează blocarea datelor (primul utilizator care le accesează, le blochează);
cu cat dimensiunea datelor blocate este mai mică, cu atât gestionarea accesului concurențial este mai
eficientă.

Coerenţa este asigurată cu ajutorul conceptului de tranzacţie. Tranzacţia este unitatea logică de lucru
constând din una sau mai multe instrucţiuni SQL, care trebuie să fie executate atomic (ori se execută
toate, ori nu se execută nici una!), asigurând astfel trecerea BD dintr-o stare coerentă în altă stare
coerentă. Dacă toate operaţiile ce constituie tranzacţia sunt executate şi devin efective, spunem că
tranzacţia este validată (COMMIT), iar modificările (INSERT, DELETE, UPDATE) aduse de

96
tranzacţie devin definitive (modificările sunt înregistrate şi sunt vizibile tuturor utilizatorilor). Din
acest punct prima instrucţiune SQL executabilă va genera automat începutul unei noi tranzacţii.

Dacă dintr-un motiv sau altul (neverificarea condiţiilor, accesul imposibil) o operaţie a tranzacţiei nu a
fost executată spunem că tranzacţia a fost anulată (ROLLBACK). Modificările aduse de toate
operaţiile tranzacţiei anulate sunt şi ele anulate şi se revine la starea bazei de date de dinaintea
tranzacţiei anulate. Executarea unei instrucţiuni ROLLBACK presupune terminarea tranzacţiei curente
şi începerea unei noi tranzacţii.

Este posibil ca o tranzacţie să fie descompusă în subtranzacţii, astfel încât dacă este necesar să se
anuleze doar parţial unele operaţii.

Controlul tranzacţiilor constă în:


• definirea începutului şi sfârşitului unei tranzacţii;
• validarea sau anularea acesteia;
• o eventuală descompunere în subtranzacţii.

Limbajul pentru controlul datelor (LCD) permite salvarea informaţiei, realizarea fizică a modificărilor
în baza de date, rezolvarea unor probleme de concurenţă.

Limbajul conţine următoarele instrucţiuni:


• SET AUTO[COMMIT] {ON | OFF} - Dacă se foloseşte utilitarul SQL*Plus, există
posibilitatea ca după fiecare comandă LMD să aibă loc o permanentizare automată a datelor
(un COMMIT implicit).
Dacă este setată pe ON, fiecare comandă LMD individuală duce la salvarea modificărilor,
imediat ce este executată. Nu se mai poate reveni la situaţia dinainte (un rollback nu mai este
posibil).
Dacă este setată pe OFF, COMMIT poate fi dată explicit. De asemeni, COMMIT este
executată odată cu o comanda LDD sau la ieşirea din SQL*Plus;
• ROLLBACK [TO [SAVEPOINT] savepoint] - permite restaurarea unei stări
anterioare a bazei de date.
Dacă nu se specifică nici un SAVEPOINT, toate modificările făcute în tranzacţia curentă sunt
anulate, iar dacă se specifică un anumit savepoint, atunci doar modificările de la acel
savepoint până în momentul respectiv sunt anulate;
• ROLLBACK TO SAVEPOINT name - şterge savepoint-ul şi toate schimbările de după el
(temporare);
• SAVEPOINT name - folosită în conjuncţie cu instrucţiunea ROLLBACK, pentru definirea
unor puncte de salvare în fluxul programului. Punctele de salvare pot fi considerate ca
nişte etichete care referă o submulţime a schimbărilor dintr-o tranzacţie, marcând efectiv
un punct de salvare pentru tranzacţia curentă. În acest mod este posibilă împărţirea
tranzacţiei în subtranzacţii.
Punctele de salvare nu sunt obiecte ale schemei, prin urmare, nu sunt referite în dicţionarul
datelor. Server-ul Oracle implementează un punct de salvare implicit pe care îl mută
automat după ultima comandă LMD executată.
Dacă este creat un punct de salvare având acelaşi nume cu unul creat anterior, cel definit
anterior este şters automat.
SAVEPOINT savepoint;

Starea datelor înainte de COMMIT sau ROLLBACK este următoarea:


• starea anterioară a datelor poate fi recuperată;
• utilizatorul curent poate vizualiza rezultatele operaţiilor LMD prin interogări asupra tabelelor;
97
• alţi utilizatori nu pot vizualiza rezultatele comenzilor LMD făcute de utilizatorul curent (read
consistency);
• înregistrările (liniile) afectate sunt blocate şi, prin urmare, alţi utilizatori nu pot face schimbări
în datele acestor înregistrări.

Execuţia unei comenzi COMMIT implică anumite modificări:


• toate schimbările (INSERT, DELETE, UPDATE) din baza de date făcute după anterioara
comandă COMMIT sau ROLLBACK sunt definitive. Comanda se referă numai la
schimbările făcute de utilizatorul care dă comanda COMMIT;
• toate punctele de salvare vor fi şterse;
• starea anterioară a datelor este pierdută definitiv;
• toţi utilizatorii pot vizualiza rezultatele;
• blocările asupra liniilor afectate sunt eliberate; liniile pot fi folosite de alţi utilizatori pentru a
face schimbări în date.

Execuţia unei comenzi ROLLBACK implică anumite modificări:


• anulează tranzacţia în curs şi toate modificările de date făcute după ultima comandă
COMMIT;
• sunt eliberate blocările liniilor implicate;
• nu şterge un tabel creat prin CREATE TABLE. Eliminarea tabelului se poate realiza doar prin
comanda DROP TABLE.

8. Exerciţii de fixare a noţiunilor

1. Definiţi următorii termeni:


- bază de date
- sistem de gestiune a bazei de date
- entitate
- relație
- atribut
- bază de date relaţională

2. Descrieți pașii pentru construirea unei diagrame E-R.

3. Daţi exemple de:


- relaţie de tip multe la multe
- relaţie de tip una la una
- relaţie de tip una la multe

4. Enunțați regulile lui Codd pentru SGBD-urile relaţionale.

5. Descrieți componentele bazelor de date relaționale.

6. Explicaţi următoarele noţiuni:


- restricţii de integritate
- cheia primară a unei relaţii
- cheia externă a unei relații

98
7. Precizaţi care sunt restricţiile de integritate minimală ale modelului relaţional, apoi enunţaţi aceste
restricţii.

8. Ce înseamnă dependenţă funcţională? Daţi două exemple de DF.

9. Definiţi următoarele noţiuni:


- atribut simplu (atomic)
- atribut compus

10. Când o relaţie este în:


- forma normală 1?
- forma normală 2?
- forma normală 3? Dați exemple.

11. Descrieți elementele unei instrucţiuni.

12. Operatori SQL.

13. Tipuri de funcții scalare.

14. Exemple de funcții de grup.

15. Sintaxa generală a comenzii SELECT.

16. Sintaxa generală a instrucţiunii INSERT.

17. Sintaxa generală a instrucţiunii UPDATE.

18. Sintaxa generală a instrucţiunii DELETE.

19. Comenzile CREATE, ALTER, DROP.

20. Adăugarea/suprimarea unei constrângeri.

21. Activarea şi/sau dezactivarea unei constrângeri.

22. Definiți noțiunea de tranzacție. În ce constă controlul tranzacţiilor?

23. Care sunt modificările pe care le implică execuţia unei comenzi COMMIT? Dar ROLLBACK?

24. Asigurarea confidențialității și securității datelor.

25. Reluarea unor acțiuni în cazul unei defecțiuni.

26. Se dă următorul tabel:


STUDENT(id_student#, nume, prenume, data_nasterii, localitate)
Ce va determina execuția comenzii următoare?

99
SELECT id_student, nume, prenume
FROM student
WHERE localitate=’Craiova’
AND TO_CHAR(data_nasterii,’MM-YYYY’)=’07-1993’;

27. Se dă următorul tabel:


TABLOU(id_tablou#, titlu, pictor, an_creare)
Ce va determina execuția comenzii următoare?

INSERT INTO tablou(id_tablou, titlu, an_creare)


VALUES (22, ‘Camp cu maci’, 1850);

28. Se dă tabelul:
ANGAJATI(cod_ang#, nume, pren, data_angajarii, salariu,
cod_manager)
Ce va determina execuția comenzii următoare?

SELECT nume, salariu


FROM salariati
WHERE cod_manager =
(SELECT cod_ang
FROM salariati
WHERE UPPER(nume) ='POPESCU' AND UPPER(pren) ='ION' );

29. Pentru acționarii unei firme se consideră tabelul următor:


ACTIUNI (id_actionar#, nume, seriain, seriaout, valoare)
(unde seriain și seriaout reprezintă seria de început, respectiv de sfârșit al intervalului de acțiuni pe
care îl are un acționar).
Ce va determina execuția comenzii următoare?

SELECT SUM((seriaout-seriain+1)*valoare))
FROM actiuni;

30. Se dă următorul tabel:


MELODII(id_melodie#, titlu, textier, compozitor, gen, durata)
Ce va determina execuția comenzii următoare?

SELECT *
FROM melodii
WHERE textier IS NOT NULL;

31. Se dă următorul tabel:


ANGAJATI(id_angajat#, nume, prenume, data_angajarii, salariu,
comision, functia)
Ce va determina execuția comenzii următoare?

SELECT nume, functia, salariu


FROM angajati
WHERE salariu > ALL(SELECT salariu
FROM angajati
WHERE functia=’Programator IT’);

100
32. Se dă următorul tabel
MASINA(id_masina#, denumire, pret_unitar, stoc_curent, categorie,
localitate)
Ce va determina execuția comenzii următoare?

SELECT localitate, categorie, SUM(pret_unitar*stoc_curent)


FROM masina
GROUP BY ROLLUP (localitate, categorie);

33. Se dă următorul tabel:


ANGAJATI(id_angajat#, nume, prenume, data_angajarii, salariu,
functia, id_sef)
Ce va determina execuția comenzii următoare?

SELECT id_angajat, nume


FROM angajati
WHERE id_angajat IN (SELECT DISTINCT id_sef
FROM angajati);

34. Se dă următorul tabel:


SALARIATI(id_angajat#, nume, prenume, cnp, data_angajarii,
salariu)
Ce va determina execuția comenzii următoare?

ALTER TABLE SALARIATI


ADD (cod_functie NUMBER(2), email CHAR(25));

35. Se dă următorul tabel:


SALARIATI(id_angajat#, nume, prenume, cnp, data_angajarii,
salariu, cod_fuctie, email)
Ce va determina execuția comenzii următoare?

ALTER TABLE SALARIATI


DROP (cod_functie, email);

36. Se dau următoarele tabele:


TABLOU(id_tablou#, titlu, data_crearii, valoare, cod_pictor)
PICTOR(id_pictor#, nume, prenume, data_nasterii)
Ce va determina execuția comenzii următoare?

SELECT nume
FROM pictor, tablou
WHERE id_pictor = cod_pictor
GROUP BY nume
HAVING SUM(valoare) > = 200000;

37. Se dau următoarele două tabele:


FIRMA(id_firma#, denumire, oras)
ANGAJAT(id_angajat#, nume, prenume, salariu, cod_firma)
Ce va determina execuția comenzii următoare?

101
SELECT id_firma, denumire, AVG(salariu)
FROM angajat, firma
WHERE id_firma=cod_firma
AND oras=’Bucuresti’
GROUP BY id_firma, denumire;

38. Se dau următoarele două tabele:


CLIENTI(id_client#, nume, prenume, data_nasterii, id_agent)
AGENTI_IMOBILIARI(id_agent#, nume, prenume, salariu, comision)
Ce va determina execuția comenzii următoare?

SELECT c.nume client, a.id_agent, a.nume agent


FROM clienti c LEFT OUTER JOIN agenti_imobiliari a
ON(c.id_agent = a.id_agent);

39. Se dau următoareale două tabele:


TABLOU (id_tablou#, denumire, data_crearii, valoare, cod_pictor)
PICTOR(id_pictor#, nume, prenume, data_nastere)
Ce va determina execuția comenzii următoare?

SELECT cod_pictor, denumire, valoare


FROM tablou t1
WHERE valoare = (SELECT MAX(valoare)
FROM tablou t2
WHERE t1.cod_pictor = t2.cod_pictor);

40. Se dau următoareale două tabele:


TABLOU(id_tablou#, denumire, data_crearii, valoare, cod_pictor)
PICTOR(id_pictor#, nume, prenume, data_nastere)
Ce va determina execuția comenzii următoare?

SELECT nume, prenume


FROM pictor
WHERE id_pictor IN (SELECT cod_pictor
FROM tablou
WHERE valoare >50000);

41. Se dau următoarele două tabele:


OPERA_ARTA(id_opera#, titlu, data_creare, nume_autor, pret,
cod_gen)
GEN(id_gen#, denumire, descriere)
Ce va determina execuția comenzii următoare?

UPDATE opera_arta
SET pret = pret * 0.5
WHERE cod_gen IN
(SELECT id_gen
FROM gen
WHERE denumire NOT IN ( ‘pictura’, ‘sculptura’));

102
42. Se dau următoarele tabele:
PRODUS(id_produs#, denumire, culoare, pret_unitar, stoc_curent,
cod_depozit)
DEPOZIT(id_depozit#, denumire, oras)
Observatie: Stocul curent nu poate fi mai mic decat 1.
Ce va determina execuția comenzii următoare?

SELECT d.denumire
FROM produs p, depozit d
WHERE p.cod_depozit = d.id_depozit
GROUP BY d.denumire
HAVING COUNT(*) >= 1000;

43. Dacă în tabelul ANGAJAT sunt menținute informații despre angajați, respectiv despre
departamentul și jobul pe care lucrează în prezent, iar în tabelul ISTORIC_ANGAJAT informații
despre departamentele și joburile pe care au lucrat aceștia în trecut, atunci ce va determina
execuția comenzii următoare?

SELECT id_angajat, cod_departament, cod_job


FROM angajat
INTERSECT
SELECT id_angajat, cod_departament, cod_job
FROM istoric_angajat;

44. Se dau următoarele trei tabele:


FACTURA(id_factura#, data_emitere)
CONTINE(cod_factura#, cod_produs#, cantitate)
PRODUS(id_produs#, denumire, pret_unitar)
Observație: Facturile conțin produsele vândute.
Ce va determina execuția comenzii următoare?

SELECT id_factura, SUM(cantitate*pret_unitar)


FROM contine c, produs p, factura f
WHERE c.cod_produs = p.id_produs
AND c.cod_factura = f.id_factura
AND TO_CHAR(data_emitere,’YYYY’) =TO_CHAR(sysdate,’YYYY’)
GROUP BY id_factura;

45. Se dau urmatoarele trei tabele:


ANGAJAT(id_angajat#, nume, prenume, data_angajarii)
LUCREAZA(id_angajat#, id_proiect#, data_inceput, data_sfarsit)
PROIECT(id_proiect#, denumire, descriere, data_lansare,
data_predare)
Ce va determina execuția comenzii următoare?

SELECT p.id_proiect, denumire, nume


FROM angajat, lucreaza l, proiect p
WHERE l.id_angajat = angajat.id_angajat
AND lucreaza.id_proiect = p.id_proiect
AND data_sfarsit<sysdate;
103
46. Se dau urmatoarele trei tabele:
ANGAJAT(id_angajat#, nume, prenume, data_angajarii)
LUCREAZA(id_angajat#, id_proiect#)
PROIECT(id_proiect#, denumire, descriere, data_lansare,
data_predare)
Ce va determina execuția comenzii următoare?

SELECT id_angajat, nume


FROM angajat
WHERE id_angajat IN (SELECT l.id_angajat
FROM lucreaza l, proiect p
WHERE l.id_proiect = p.id_proiect AND
TO_CHAR(data_lansare,’YYYY’) = 2017;

47. Se dă următorul tabel:


ANGAJAT(id_angajat#, nume, prenume, data_angajarii, job, sal,
ore_suplimentare, plata_ora)
Ce va determina execuția comenzii următoare?

UPDATE angajat
SET plata_ora = ROUND(plata_ora * 1.1, 2)
WHERE job =‘IT‘ ;

48. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Ce va determina execuția comenzii următoare?

SELECT mgr, MIN(sal)


FROM emp
WHERE mgr IS NOT NULL
GROUP BY mgr
HAVING MIN(sal) > 3000
ORDER BY MIN(sal) DESC

49. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Ce va determina execuția comenzii următoare?

SELECT YEAR(hiredate) [Anul angajarii],


COUNT(hiredate) [Nr. de angajati]
FROM emp
GROUP BY YEAR(hiredate)

50. Se dă următorul tabel:


DATE_PERSOANA(CNP, nume, prenume, nr_telefon, simbol_judet)
Ce va determina execuția comenzii următoare?

104
SELECT CONCAT('Numele: ',nume) AS numele,
CONCAT('Anul: ', '19',SUBSTR(CNP,2,2),' , ',
'Luna:',SUBSTR(CNP,4,2), ', ',
'Ziua: ',SUBSTR(CNP,6,2)) AS data_nasterii
FROM DATE_PERSOANA;

51. Se dă următorul tabel:


SALARIATI(id_angajat#, nume, prenume, cnp, data_angajarii,
salariu, cod_fuctie)
Scrieți comanda care șterge din tabelul SALARIATI coloanele cod_fuctie și email.

52. Se dă următorul tabel:


ANGAJATI(id_angajat#, nume, data_angajarii, salariu, functia,
id_sef)
Scrieți comanda care obține numărul de angajați care au șef.

53. Se dă următorul tabel:


SALARIATI(id_angajat#, nume, prenume, cnp, data_angajarii,
salariu, cod_fuctie)
Scrieți comanda care afișează numele salariaților care câștigă mai mult decât salariul mediu pe
companie, în ordine crescătoare a salariului.

54. Pentru tabelele:


PROFESORI(id_prof#, nume, pren, salariu)
COPII(id_copil#, nume_copil, varsta, id_prof)
Scrieți secvența pentru a afișa profesorii fără copii?

55. Se dau următoarele două tabele:


ANGAJAT (id_angajat#, nume, prenume, data_angajarii, id_job,
id_departament)
ISTORIC_JOB (id_angajat#, data_inceput#, data_sfarsit, id_job,
id_departament)
Observație: Tabelul ANGAJAT conține informații prezente referitoare la departamentul și jobul
angajatului, iar tabelul ISTORIC_JOB conține informații referitoare la trecut.
Scrieți comanda care obține codul, numele și prenumele tuturor angajatilor firmei care au lucrat la un
moment dat în departamentul 30 și în prezent lucrează în alt departament.

56. Se dau următoarele două tabele:


TABLOU(id_tablou#, titlu, an_creatie, nume_pictor, id_sala)
SALA(id_sala#, denumire, capacitate)
Scrieți comanda care afișează toate sălile în care nu sunt expuse tablouri.

57. Se dă următorul tabel:


STUDENTI (nume, prenume, nrmatricol, grupa, datan, adresa, media)
Pentru fiecare grupă se cere numărul de studenti, media minimă şi media maximă.

58. Se dau următoarele două tabele:


STUDENTI (nume, prenume, nrmatricol, grupa, datan, adresa, media)
PROFILE (denumire, cod_grupa)
Se cer studenţii de la grupele cu profil electric(ELE) sau mecanic(MEC), care au media mai mare
decât media generală a studenţilor de la aceste grupe.
105
59. Se dă următorul tabel:
PROFESOR(Cod_Prof#, Nume, Disciplina, DataN, Telefon)
Se cere numărul de profesori născuţi în acelaşi an.

60. Se dă următorul tabel:


PROFESOR(Cod_Prof#, Nume, Disciplina, DataN, Telefon)
Se cer profesorii care sunt născuţi în număr de 2 în acelaşi an.

61. Se dau următoarele două tabele:


SCOALA(Cod_Scoala#, Nume, Adresa, Cod_Director, Cod_Prof)
PROFESOR(Cod_Prof#, Nume, Disciplina, DataN, Telefon)
Se cer toate datele despre profesorii de la şcoala cu codul ’S308’, cu vârsta mai mică decât vârsta
medie a profesorilor (din toate şcolile).

62. Se dau următoarele două tabele:


SCOALA(Cod_Scoala#, Nume, Adresa, Cod_Director, Cod_Prof)
PROFESOR(Cod_Prof#, Nume, Disciplina, DataN, Telefon)
Se cer toate datele despre profesorii care au cel putin un director mai tânăr decât ei.

63. Se dau următoarele două tabele:


PROFESORI(cod_profesor#, nume, prenume, salariu, cod_facultate)
TOTALURI(cod_facultate#, nr_profesor, total_salarii)
Folosind informațiile din tabelul PROFESORI, se cere să se insereze în tabelul TOTALURI numărul
de profesori și suma totală a salariilor pentru fiecare facultate.

64. Se dă următorul tabel:


DATE_PERSOANA(CNP, nume, prenume, nr_telefon, simbol_judet)
Modificaţi toate numerele de telefon din judeţul Maramureş, astfel ca prefixul să nu mai fie 0262 ci
0362.

65. Se dau următoarele trei tabele:


FURNIZORI (id_furnizor#, numeF, oras)
COMPONENTE(cod_componenta#, numeC, culoare, localitate)
FURNIZORI_COMPONENTE(id_furnizor#, cod_componenta#, cantitate)
Se presupune că fiecare furnizor este localizat în exact un singur oraş.
Se cere numele furnizorilor ce au livrat componenta C2 şi care sunt oraşele din care provin aceşti
furnizori.

66. Se dau următoarele trei tabele:


FURNIZORI (id_furnizor#, numeF, oras)
COMPONENTE(cod_componenta#, numeC, culoare, localitate)
FURNIZORI_COMPONENTE(id_furnizor#, cod_componenta#, cantitate)
Se presupune că fiecare furnizor este localizat în exact un singur oraş.
Se cere să se determine componenta roşie care s-a livrat în cea mai mare cantate.

67. Se dau următoarele trei tabele:


FURNIZORI (id_furnizor#, numeF, oras)
COMPONENTE(cod_componenta#, numeC, culoare, localitate)
FURNIZORI_COMPONENTE(id_furnizor#, cod_componenta#, cantitate)
106
Se presupune că fiecare furnizor este localizat în exact un singur oraş.
Se cere să se determine din ce oraş provine furnizorul cu cele mai puţine componente vândute, şi care
sunt aceste componente.

68. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze angajaţii cu acelaşi job ca şi angajatul cu codul 7369 şi cu salariul mai mare decât al
angajatului cu codul 7876.

69. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze toate departamentele cu salariul minim mai mare decât salariul minim din departamentul
20.

70. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze codul, numele şi jobul angajaţilor cu salariul mai mic decât cel al oricărui funcţionar şi
care un sunt funcţionari.

71. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze angajaţii (nume, nr. departament, salariu şi comision) ale căror salariu şi comision sunt
identice cu cele ale oricărui angajat din departamentul 30.

72. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze numele angajatului cu cea mai mare vechime în muncă şi cel mai nou în firmă.

73. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze maximul, minimul şi suma salariilor pentru fiecare tip de job, în cadrul fiecărui
departament.

74. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze angajaţii care au subordonaţi (sunt şefi).

75. Se dau următoarele două tabele:


EMP(empno#, ename, job, mgr, hiredate, sal, comm, deptno)
DEPT(deptno#, dname, loc)
Să se afişeze salariul total plătit pe fiecare job în cadrul fiecărui departament.

107
Bibliografie

1. Bâscă, O., Baze de date, Editura All, 1997;


2. Cârstoiu, D., Baze de date, Editura Matrix ROM, 2009;
3. Connolly T., Begg C., Database Systems. A Practical Approach to Design, Implementation,
and Management, Ed. Addison Wesley, 2005
(http://www.palinfonet.com/download/software2/database%20systems.pdf);
4. Fusaru D., Arhitectura bazelor de date-mediul SQL, Editura Fundației România de Mâine,
2002;
5. Popa M., Baze de date (Fundamente, exemple, teste de verificare), Editura Fundației România
de Mâine, București 2006;
6. Popescu I., Modelarea bazelor de date, Editura Tehnică, 2002;
7. ***, Informatică pentru examenul de licență, Universitatea SPIRU HARET, 2017;
8. ORACLE, https://www.oracle.com/index.html.

108
109
III. Structuri de date
Dana-Mihaela Vîlcu, Lector univ. dr., Universitatea Spiru Haret

Înțelegem prin structură de date o modalitate de organizare a datelor folosite în programare. Structurile
de date avute în vedere în lucrarea de față sunt structurile elementare: tablouri, liste, stive, cozi și
arbori, cu precădere arborii binari. Modul de organizare al fiecărei structuri de date este definitoriu
acesteia. De aceea, orice operație care se efectuează asupra unei structuri de date nu trebuie să-i
altereze proprietățile induse de organizarea ei. Pentru fiecare din structurile menționate sunt indicate,
în primul capitol: definiția, exemple, proprietăți, operații de bază. O secțiune aparte prezintă operații
mai complexe, respectiv sortarea și căutarea datelor, cu algoritmii lor specifici. Capitolul al doilea
aduce probleme de consolidare a înțelegerii conceptelor prezentate. Unele dintre acestea sunt întrebări
cu răspuns direct din capitolul teoretic, altele necesită rezolvăr care se obțin prin aplicare imediată a
noțiunilor teoretice. Cel de al treilea capitol este de aprofundare și exersare, conținând probleme
pentru a căror rezolvare este nevoie de aplicarea și utilizarea noțiunilor teoretice într-un mod mai
elaborat, mai amplu.

Pentru elaborarea acestui material am avut în vedere faptul că aceste noțiuni trebuie să facă parte din
cultura generală a oricărui informatician. Am dorit ca materialul să fie, totodată, un îndrumar și o cheie
de verificare a cunoștințelor pentru structurile de date menționate, util în pregătirea pentru a face față
unei tirade de întrebări de interviu la care se așteaptă răspuns imediat, util în pregătirea examenului de
finalizare a studiilor de liecnță..

Menționăm că am preluat informațiile așa cum sunt ele prezentate în literatura de specialitate indicată
în bibliografie, dar am intervenit la nivel de formă pentru a le da un caracter unitar și succint,
corespunzător cerințelor acestei lucrări. Lucrarea [1] a reprezentat suportul de curs al promoției 2017
pentru studenții la Informatică din cadrul Universității Spiru Haret, la disciplina Structuri de date. De
aceea această lucrare este referința principală pentru partea teoretică a materialului de față, cuprinsă în
capitolul întâi. În ceea ce privește problemele incluse în capitolele 2 și 3, unele fac parte deja din
„folclor”, ele regăsindu-se, într-o formă sau alta, pe numeroase site-uri, lucrări, manuale, cărți. Ele nu
îmi aparțin însă nici nu li se poate identifica autorul. Există și probleme mai aparte, îndeosebi în
ultimul capitol, pentru care referințele sunt clare și sunt specificate ca atare.

O desenare facilă a arborilor ne-a fost înlesnită de utilizarea editorului yEd Graph Editor
(http://www.yWorks.com).

1. Noțiuni teoretice – prezentare


1.1 Structuri de date statice: vectori, matrici

Matricele intervin în numeroase aplicații, nu doar matematice, de calcul științific. Astfel, valorile
numerice stocate pot avea semnificații aparte, matricea nefiind decât o modalitate de aranjare a acestor
date în contextul problemei (vezi, de exemplu, reprezentrea grafurilor și numeroasele probleme care se
reduc la probleme pe grafuri). Mai mult, se folosește adesea o extindere a ideii de matrice, ca
modalitate de structurare a unor date de același tip, nu neapărat numeric. Asemenea structurări de date
le găsim în programare, în mod uzual, sub denumirea de tablou (en. array).

Implementarea matricilor poate fi făcută static sau dinamic. Punem în evidență, în cele de mai jos,
elementele fundamentale ale implementării statice, pentru diverse tipuri de matrici.

110
Înțelegem prin vector o mulțime finită și ordonată în care:
 numelui vectorului, v, i se asociază o adresă unică de memorie ce reprezintă locația în
care se memorează primul element al vectorului (numele vectorului memorează adresa
fizică a primului element al vectorului);
 elementele vectorului se stochează în spațiul programului, într-o zonă compactă de locații
libere, succesive;
 dimensiunea spațiului de memorare a vectorului este dată de produsul dintre numărul
elementelor vectorului și dimensiunea unui element, conform tipului său.

Numerotarea elementelor unui vector începe de la 0. Indicele, sau poziția relativă a fiecărui element în
cadrul vectorului se mai numește și rang al elementului în cadrul vectorului. Astfel, al m-lea element al
vectorului are rangul m-1 (este elementul v[m-1]).

Implementarea tablourilor bidimensionale (a matricilor) se bazează pe implementarea statică a


vectorilor și se face prin linearizare linie (C++, Java) sau coloană (FORTRAN) a elementelor matricii
date. Astfel, numele matricii conține adresa primului element al acesteia. Celelalte elemente se
stochează în locații succesive de memorie din spațiul programului, unele după altele, în ordinea liniilor
(întâi elementele primei linii, apoi cele de pe linia a doua, ș.a.m.d.) sau a coloanelor, după felul
linearizării.

Se consideră rang al elementului unui tablou, în cadrul vectorului obținut la implementarea statică,
prin linearizarea tabloului, ca fiind poziția relativă a elementului în cadrul tabloului. Astfel, într-o
linearizare linie, a unei matrici a de dimensiune m x n, rangul elementului ai,j este (i-1)*n+j-
1.

În cazul implementării matricilor particulare, pentru optimizarea spațiului ocupat, se recurge la


stocarea numai a părții potențial nenule, de asemenea prin linearizare linie sau coloană.

O matrice pătrată, nxn, este superior (inferior) triunghiulară dacă toate elementele de sub (deasupra)
diagonalei principale sunt nule. Stocând numai elementele diagonalei principale și a celor de deasupra
ei, dimensiunea vectorului de linearizare devine n*(n+1)/2. Putem vorbi despre rang al unui
elementului, ai,j, numai dacă acesta apare în vectorul de linearizare. Astfel, pentru o matrice superior
(inferior) triunghiulară se au în vedere numai elementele pentru care i ≤ j (respectiv i ≥ j), iar
valoarea rangului se determină ca [n+(n-1)+…+(n-i+1)]+(j-i+1)-1. În mod asemănător se
fac calculele și pentru situațiile de linearizare coloană, respectiv pentru matrice inferior triunghiulară,
separat pentru fiecare din cele două tipuri de linearizări.

O situație similară matricilor triughiulare avem la implementarea matricilor simetrice (pentru care ai,j
= aj,i, pentru orice i ≠ j).

În cazul matricilor diagonale (elemente nenule pot fi numai pe diagonala principală, respectiv pe cele
k diagonale imediat alăturate acesteia, pentru matricea k-diagonală), implementarea se poate face și
prin stocarea elementelor de pe diagonale în ordinea diagonalelor, dar și obișnuit, prin linearizare linie
sau coloană, cu păstrarea numai a elementelor diagonalelor.

1.2 Liste

Înțelegem prin listă (listă liniară) o organizare conceptuală a unor date de același tip sub forma unui
șir, eventual vid. O listă nevidă conține un prim și un ultim element, nu neaparat distincte. În cadrul
oricărei aplicații, prima referire la o listă trebuie să fie inițializarea ei (cel mai adesea ca listă vidă).
111
Dintre operațiile care se pot efectua asupra listelor menționăm și avem în vedere în cele ce urmează:
 accederea la un element pe baza poziției sau pe baza valorii sale,
 inserarea unui element într-o poziție dată prin numărul de ordine sau relativ la valoarea
unui alt element din listă,
 eliminarea unui element dat prin poziție sau valoare,
 concatenarea a două liste,
 descompunerea unei liste în două sau mai multe liste,
 realizarea copiei unei liste,
 ordonarea elementelor unei liste,
 interclasarea a două sau mai multe liste.

Dacă lista este de un tip particular, efectuarea operațiile menționate trebuie să conserve
particularitatea listei inițiale și să transfere proprietățile și eventualelor liste rezultate.

Implementarea listelor se poate face secvențial sau înlănțuit.

1.2.1 Liste implementate secvențial

Implementarea secvențială presupune stocarea elementelor listei în ordine, în locații de memorie


succesive, cu memorarea adresei primului element. Realizarea practică se face cu ajutorul unui vector.
Având în vedere, pe de o parte, caracterul fix al spațiului alocat inițial, conform dimensionalității
vectorului, dar, pe de altă parte, faptul că lungimea listei este variabilă, este necesară și memorarea
poziției ultimului element din listă.

Implementarea operațiilor menționate anterior trebuie să aibă în vedere stocarea listei ca vector, cu
memorarea poziției primului element al listei, și continuitatea locațiilor de memorie. Astfel, adăugarea
sau scoaterea unui element din listă induce și deplasări ale celorlalte elemente din listă ceea ce face
aceste operații costisitoare. De remarcat însă accederea rapidă la un element dat prin poziția sa. Pe de
altă parte, din caracterul static al vectorilor, spațiul utilizat pentru stiva programului fiind constant și
declarat de la început, poate fi mult mai mare decât spațiul efectiv utilizat pentru stocarea listei
propriu-zise.

1.2.2 Liste înlănțuite

Implementarea înlanțuită a listelor liniare presupune stocarea fiecărui element al listei într-o celulă
formată din două părți, conținând:
 datele corespunzătoare elementului de stocat și, respectiv,
 informație de legătura către elemental următor din listă (de regulă adresa acestuia).

Astfel, considerăm pentru configurarea structurii unui nod:


node->data = valoarea datelor stocate în elementul nod,
node->next = adresa elementului următor.

Se memorează adresa primului element în V, iar partea de legătură a ultimului element primește o
valoare ce nu poate desemna nicio legătură (NULL).

112
Implementarea înlănțuită elimină necesitatea efectuării de deplasări ale elementelor listei la adăugarea
sau scoaterea unui element din listă. De asemenea, în implementarea înlănțuită dinamică, prin
eliberarea spațiului ocupat de celule care se șterg se obține o mai bună gestionare a memoriei.

În descrierea operațiilor de prelucrare a listelor avem în vedere noțiunile de:


 overflow (depășire superioară) – întregul spațiu alocat structurii considerate este
ocupat și se încearcă adăugarea încă a unui element, ceea ce face ca operația să eșueze.
 underflow (depășire inferioară) – structura considerată este vidă și se încearcă
eliminarea unui element, ceea ce face ca operația să eșueze.

De asemenea avem în vedere apelarea unor rutine de alocare și eliberare a memoriei, numite în cele de
mai jos:
 alloc() - pentru alocarea unei celule de memorie și returnarea adresei ei, și
 free(k) - pentru eliberarea celulei a cărei adresă se transmite ca parametru.

În cadrul algoritmilor de mai jos considerăm făcute apriori declararea tuturor variabilelor
suplimentare, precum și alocarea spațiului necesar lor cu verificarea disponibilității acestuia sub
forma:
node = alloc();
if (node == NULL)
then overflow // tratare caz heap plin
else { // operația de efectuat

}

Pentru o bună gestionare a memoriei și evitarea pointării către obiecte care nu sunt valide, eliberarea
unei celule va fi urmată imediat de inițializarea zonei cu NULL:
free(k); k = NULL;

Dezavantajul alocării dinamice este dat de necesitatea parcurgerii listei element cu element pentru
fiecare operație. Pentru o parcurgere mai facilă se pot aborda și alte metode de implementare a
informației de legătură, ce duc astfel la liste particulare cum ar fi:
 lista dublu înlănțuită - păstrează două câmpuri de legătură, pentru memorarea adresei
elementului următor și a celui predecesor;
 lista circulară - valoarea câmpului de legătură al ultimului element este adresa primului
element din listă.

Implementarea dinamică a unei liste simplu înlănțuite

Inițializarea listei
V = NULL;

Adăugarea elementului cu valoarea x ca prim element


node->data = x; node->next = V; V = node;

Adăugarea elementului cu valoarea x ca ultim element

113
node->data = x; node->next = NULL; // nodul de adaugat
if (V == NULL) // lista e vida
then V = node;
else
{ // parcurgerea listei pana la ultimul nod
p = V;
while (p->next != NULL)p = p->next;
// inserarea noului nod
p->next = node;
}

Inserarea elementului cu valoarea x după al n-lea element din listă


node->data = x; node->next = NULL; // nodul de inserat
p = V, i = 1
while (p != NULL and i < n){
p = p->next; i = i + 1;
}
if( p == NULL)
then overlist /* lista nu are n noduri; tratament particular */
else {
node->next = p->next; p->next = node;
}

Extragerea primului element din listă, cu salvarea în z a datelor corespunzătoare lui


if (V == NULL)
then underflow // nu se poate scoate niciun element, lista e vida
else
{
node = V; z = node->data; // extragerea primului nod
V = node->next; // refacerea listei
free(node); // eliberarea celulei corespunzatoare lui node
node = NULL; // pentru control asupra continutului lui node
}

Extragerea ultimului element din listă, cu salvarea în z a datelor corespunzătoare lui


if (V == NULL)
then underflow
else
{ // se parcurge lista cu doi pointeri consecutivi p și u
// pana cand p, primul, ajunge pe ultimul element
u = NULL; p = V;
while (p->next != NULL)
{
u = p; p = p->next;
}
if (u == NULL) // lista are un singur element
then V = NULL;
else u->next = NULL;
114
z = p->data; free(p); p = NULL;
}

Extragerea celui de al n-lea element din listă, cu salvarea în z a datelor corespunzătoare lui
if (V == NULL)
then underflow
else
{ // p parcurge lista pana la al t-lea element
// u adresa predecesorului lui p
if (V == NULL)
then underflow;
else
u = NULL; p = V; t = 1;
while (p != NULL and t < n)
{
u = p; p = p->next; t = t + 1 ;
if (p == NULL)
then overlist
else
if (u == NULL)
then V = NULL;
else u->next = p->next;
}
z = p->data; free(p); p = NULL;

1.2.3 Liste dublu înlănțuite

Introducerea dublei înlănțuiri are ca scop o mai ușoară parcurgere a listei. Considerăm structura
nodurilor listei dublu înlănțuite dată de:
node->data = valoarea datelor stocate în elementul nod,
node->rlink = adresa elementului următor la dreapta,
node->llink = adresa elementului următor la stânga.

Pentru completarea construcției se memorarează și cele două capete ale listei firstL (pentru primul
element din stânga al listei), respectiv firstR (pentru primul element din dreapta). Fiind în același
timp și ultime elemente în sens invers, este valabil întotdeauna că firstL->llink și firstR-
>rlink să fie NULL.

Să observăm că operațiile se desfășoară asemănător celor de pe lista simplu înlănțuită. Particularitatea


dublei înlănțuiri implică percurgerea de fiecare dată cu ajutorul a doi pointeri, astfel încât, la operarea
propriuzisă în listă să se poată face legăturile atât cu nodul predecesor cât și cu nodul succesor celui
operat. Pentru exemplificarea modului de lucru cu liste dinamice dubluîlănțuite prezentăm:

Inserarea elementului cu valoare x dupa nodul cu adresa t, în sensul de la stânga la dreapta


// inserarea nodului
node->data = x; node->llink = t; node->rlink = t->rlink;
// completarea dublei legaturi din stanga nodului introdus
t->rlink = node;
115
// completarea dublei legaturi din dreapta nodului introdus
p = node->rlink ;
if (p != NULL)
then p->llink = node;
else
// nodul introdus e ultimul din lista
firstR = node;

Eliminarea nodului de la adresa t


q = t->llink;
if (q == NULL)
then firstl = t->rlink;
else q->rlink = t->rlink;
q = t->rlink;
if (q == NULL)
then firstR = t->llink;
else q->llink = t->llink;
free(t); t = NULL;

1.3 Stive

Stiva este un caz particular de listă liniară, în care accesul se realizează întotdeauna doar la unul din
capete, numit vârful stivei. Elementul de la capătul opus se numește baza stivei. Astfel, principiul de
funcționare al stivei este “ultimul venit - primul servit”, sau LIFO (en. Last In – First Out). Prin
urmare, extragerea unui anumit element oarecare din stivă se face numai numai după ce au fost extrase
elementele care au intrat în stivă după și peste acesta.

1.3.1 Stiva secvențială

Implementarea secvențială a unei stive S presupune stocarea elementelor sale într-un vector a cu m
elemente și prelucrarea după cum urmează: dacă v = vârful stivei, el se incremenentează odată cu
adăugarea unui element, respectiv se decrementează la extragerea unui element din stivă. Operațiile
elementare sunt descrise după cum urmează:

Inițializarea stivei
v = 0;

Adăugarea elementului x la stivă (x=>S)


if (v == m) //stiva este plina
then overflow ;
else { v = v+1; a[v] = x; }

Extragere unui element din stivă (z<=S)


if (v == 0) // stiva este vida
then underflow;
else { z = a[v]; v = v–1; }

116
Stive secvențiale cu spațiu partajat
Prezentăm (adaptat din [1]) modalitatea de implementare a lucrului cu mai multe stive, S 1,…,Sn, cu
informații de același tip, ce partajează același spațiu, un vector a cu m elemente. Stivele se stochează
una în continuarea celeilalte. a[1] reprezintă baza primei stive nevide. Se folosește un vector
suplimentar v, cu n+2 elemente, pentru memorarea vârfurilor stivelor: v[i] este vârful stivei S i. Se
consideră stivele fictive S0 și Sn+1 cu vârfurile în v[0], respectiv în v[n+1] (elemente fictive).

Inițializare
for(i=0; i<=n+1; i++) v[i]=0;

Adăugarea elementului x la stiva Si (x => Si)


if(v[n+1] == m) // intreg spatiul este plin
then overflow
else
{
//decalarea stivelor următoare
for(j=v[n+1]; j>=v[i]+1; j--) a[j+1] = a[j];
//adaugarea propriuzisa a elementului x
a[v[i]] = x;
//actualizarea deplasarii varfurilor precedente
for(j=i; j<=n+1; j++) v[j] = v[j]+1;
}

Extragerea elementului z din stiva Si (z<=Si)


if(v[i-1] == v[i]) //stiva i este vida
then underflow;
else
{
//extragere elementului
z=a[v[i]];
//translatarea la stanga a stivelor urmatoare
for(j=v[i]+1; j<=v[n+1]; j++) a[j–1]=a[j];
//actualizarea varfurilor stivelor
for(j=i; j<=n+1; j++) v[j]=v[j]–1;
}

1.3.2 Stiva simplu înlănțuită

Păstrând notațiile utilizate la implementarea listelor și având în vedere sensul legăturilor de la vârful
stivei spre bază, precum și a proprietăților specifice stivei, putem descrie:

Inițializarea stivei
V = NULL;

Adăugarea elementului x la stivă (x=>S)


a = alloc();
if(a==NULL) //nu se mai poate aloca spațiu in heap
then overflow ;
117
else
{
//introducerea elementului
a->data=x; a->next=V;
//refacerea stivei
V=a;
}

Extragerea unui element din stivă (z<=S)


if (V == NULL) // stiva este vida
then underflow;
else
{
//recuperarea informației
a=V; z=a->data;
//refacerea stivei
V=V->next;
//disponibilizarea celulei
free(a); a=NULL;
}

1.4 Cozi

Coada este o listă liniară în care introducerile de noi elemente se fac pe la un singur capăt (baza
cozii), iar extragerile se fac pe la capătul opus (vârful cozii). Astfel, principiul de funcționare este
“primul venit – primul servit”, sau FIFO (en. First In – First Out). De aceea scoaterea unui anumit
element din coadă, se face numai numai după ce au fost extrase elementele care au intrat în coadă
înaintea/în fața acestuia.

Coada completă este lista liniară în care depunerile și extragerile sunt permise la oricare din capetele
listei (fie el capăt stâng, sau capăt drept).

Dacă la unul dintre capetele cozii complete nu sunt permise operații de depunere (extragere) atunci
spunem că avem o coadă completă restricționată la intrare (ieșire) la acel capat.

Coada cu priorități este un caz particular de coadă în care fiecare element are asociată o valoare și o
prioritate. Cu cât valoarea priorității unui element este mai mică, cu atât elementul este mai prioritar.
Astfel, în coada cu priorități, elementele sunt ordonate crescător în raport cu prioritățile lor, de la vârf
către bază, astfel încât nodul cel mai prioritar (cea mai mică prioritate) iese primul.

Ținând cont de aspectele menționate, introducerea unui element de prioritate q se face între două
noduri consecutive cu priorități mai mică și, respectiv, mai mare, sau, în cazul mai multor noduri cu
priorități egale și egale cu cea a nodului introdus, acesta va fi primul dintre ele, dinspre bază, pe unde
se și introduce.

118
1.4.1 Coada secvențială

Implementarea secvențială a unei cozi se face cu ajutorul unui vector a de m elemente. Întotdeauna
unul din capete este a[0]. Celălalt capăt se memorează ca poziție P în vector. În cele de mai jos
prezentăm prelucrările elementare în care a[0] este baza cozii, iar P este vârful.

Inițializare
P=0;

Implementare secvențială cu decalare unitară a unei cozi

Adăugarea elementului x la coadă


if(P == m) //nu mai e loc la coada
then overflow ;
else
{
// se face loc în bază noului element
for(i = P; i>=0; i--) a[i+1]=a[i];
// se actualizează indicatorul vârfului
P = P+1;
// se introduce in baza noul element
A[0] = x;
}

Extragere unui element


if(P == 0) // coada vida
then underflow;
else { z = a[P]; P = P–1; }

Implementarea sevențială cu decalare multiplă a unei cozi


Decalarea se face doar în cazul în care depunerea ar depăși spațiul alocat vectorului în care se
stochează coada. În felul acesta se optimizează timpul general de efectuare a adăugării unui element,
prin eliminarea deplasărilor, și efectuarea tuturor acestora la un moment dat.

Pentru exemplificare considerăm că extragerea se face spre a[0], că B indică baza cozii și V elementul
de dinaintea celui din vârful cozii.

Adăugarea elementului x
if(P == m)
then
if (V == 0)
then overflow ;
else
{
for (i=V+1;i<=B;i++)
a[i-V]=a[i];
B=B-V; V=0;

119
}
B=B+1; a[B]=x;

Implementare sevențială circulară a unei cozi


Implementarea circulară are rolul de a evita efectuarea decalărilor. Ca în cazul precedent, B reprezintă
baza cozii, V elementul de dinaintea celui din varful cozii. Astfel:

Adăugarea elementului x
if(V == mod(m, B))
then overflow ;
else
{
B = mod(m, B+1);
a[B] = x;
}

Extragere unui element


if(V == B)
then underflow;
else
{
V = mod(m, V+1);
z = a[V];
}

Implementarea cozii secvențiale complete se poate face cu decalare unitară, multiplă sau circulară.
Tehnica pentru fiecare dintre acestea a fost prezentată și se aplică precum în cele de mai sus, ținând
cont că operațiile de adăugare și de scoatere a unui element se pot face pe la oricare dintre cele două
capete ale cozii. Pentru mai multe detalii se poate consulta [1].

Coada secvențială cu priorități


O variantă a implementării secvențiale a cozii cu priorități presupune folosirea a doi vectori, unul, v,
pentru stocarea valorilor propriu-zise ale elementelor, și un altul, pr, pentru stocarea priorităților
corespunzătoare valorilor stocate în v. Astfel, pentru introducerea unui element în coadă trebuie
parcurs vectorul priorităților pentru identificarea poziției pe care trebuie să intre noul element. Cei doi
vectori, de valori și de priorități se actualizează sincron. Extragerea nu pune probleme, se extrage
primul element din ambii vectori.

Adăugarea elementului x cu prioritate y


if(P == m)
then overflow ;
else
{
pr[P+1]=-1; k=1;
while(p[k] > y)
k++;
for (i=P+1; i>= k; i++)

120
{
v[i+1]=v[i];
pr[i+1]=pr[i];
}
P++;
v[k]=x; pr[k]=y;
}

Atunci când limbajul de programare permite, lucrul cu doi vectori poate fi evitat dacă se recurge la o
implementare a cozii printr-un singur vector ale cărui elemente au structură de articol.

1.4.2 Coada simplu înlănțuită

Folosim aceleași notații ca în descrierea implementării dinamice a listelor simplu înlănțuite, stocând în
fiecare nod informație utilă în data și informație de legătură către nodul următor în next. De asemenea,
considerăm făcute apriori declararea tuturor variabilelor suplimentare folosite și alocarea spațiului
necesar lor cu verificarea disponibilității acestuia.

Cele două capete ale cozii sunt B, baza, și V, vârful. Depunerile se fac la bază și extragerile din vârf, și
se desfășoară după cum urmează:

Sensul legăturilor este de la V la B

Inițializarea listei
V = B = NULL;

Adăugarea unui element


a = alloc();
if (a == NULL)
then overflow
else
{
a->data = x; a->next = NULL;
if (V == NULL)
then V=a;
else B->next=a
B=a;
}

Extragerea unui element din coadă


if (V == NULL)
then underflow
else
{
a = V; V = a->next;
if (V == NULL)
then B = NULL;
z = a->data;
121
free(a);
}

Sensul legăturilor este de la B la V


Adăugarea unui element
a = alloc();
if (a == NULL)
then overflow
else
{
a->data = x; a->next = B;
B = a;
}

Extragerea unui element din coadă


if (B == NULL)
then underflow
else
{
u = NULL; i = B;
while (i->next != NULL)
{
u = i; i = i->next;
}
if (u == NULL) //s-a scos ultimul element
then B = NULL;
else u->next = NULL;
z = i->data; free(i);
}

Coadă circulară simplu înlănțuită


Adăugarea unui element
a = alloc();
if (a == NULL)
then overflow
else
{
a->data = x;
if (V == NULL) //coada era vida
then
{
V = a; a->next = a;
};
else
{
a->next=V->next; V->next=a;
}
}
122
Extragerea unui element din coadă
if (V == NULL)
then underflow
else
{
z = V->data;
if (V->next == V) // coada avea un singur element
then
{
free(V); V = NULL;
}
else
{
a = V;
while (a->next != V)
a = a->next;
b = V;
a->next = b->next; V = a;
free(b);
}
}

1.4.3 Coada dublu înlănțuită

În cazul cozilor cu dublă înlănțuire, tehnicile de adăugare, respectiv de extragere a unui element
trebuie să aibă în vedere refacerea legăturilor în ambele sensuri. Tehnica a fost prezentată anterior pe
cazul listelor dublu înlănțuite. (Pentru detalii de implementare se poate consulta, de exemplu, [1].)

1.5 Structuri arborescente – noțiuni fundamentale

Se numește arbore structura de date alcătuită din noduri și conexiuni unidirecționale (arce) între
acestea așa încât să nu conțină cicluri.
Arborele fără noduri este un arbore vid.
Un arbore nevid are un unic nod, numit rădăcină, în care nu intră niciun arc.
Nodurile în care intră arcele care pleacă dintr-un nod se numesc noduri fii ai nodului dat, iar acesta se
numește nod părinte.
Un nod fără fii se numește nod frunză (sau nod extern).
Orice nod părinte se mai numește nod intern, sau nod de ramificare.
Gradul unui nod este dat de numărul copiilor acelui nod.
Orice nod la care se poate ajunge pe sensul arcelor de la un nod dat se numește descendent al acestuia.
Drumul dintre un nod și un descendent al său este succesiunea de noduri și arce care le leagă prin
descendență.
Nivelul unui nod = numărul de conexiuni dintre nod și rădăcină. Rădăcina are nivel 0.
Înălțimea unui nod este dată de numărul de arce al celui mai lung drum de la nod la o frunză.
Înălțimea unui arbore = înălțimea rădăcinii arborelui.
Adâncimea unui nod = numărul de arce de la rădăcină la nod.
Pădurea este mulțimea formată din cel puțin doi arbori disjuncți.
123
Figura 1-1

Cel mai adesea arborii se reprezintă planar, nodurilor fii impunându-li-se o relație de ordine. De regulă
nodurile de pe un același nivel se plasează pe o aceeași linie orizontală.

Dacă ordinea relativă a fiilor fiecărui nod este semnificativă pentru definirea arborelui, spunem că
avem un arbore ordonat.

În Figura 1-1 este reprezentat un arbore oarecare. Rădăcina sa stochează valoarea 1. Nodurile 5, 9, 10,
8 și 4 sunt noduri frunze. Nodul 3 are ca părinte pe 1 și ca fii pe 7 și 8. Nodul 10 este și el descendent
al lui 3. Drumul de la 3 la 10 este: 3, 7, 10. Rădăcina se află pe nivelul 0. Nodurile de pe nivelul 3
sunt: 9 și 10. Înălțimea nodului 3 este 2. Adâncimea nodului 3 este 1. Înălțimea arborelui, dată de
lungimea celui mai lung drum de la tădăcină la o frunză, este 3 (ex. drumul de la 1-9, sau de la 1-10).

Se numește parcurgere a arborelui o examinare sistematică a nodurilor astfel încât fiecare nod să fie
considerat o singură dată. Prin parcurgerea unui arbore se determină o ordine totală asupra nodurilor
din arbore. În acest sens, pentru fiecare nod se poate spune care nod îl precede şi care îl succede.

Arborii sunt structuri neliniare. Implementarea lor se poate face prin structuri de liste sau prin
structuri cu legături fiu-frate. Accesul la elementele arborelui se face prin intermediul unei variabile
care să indice rădăcina sa. Să observăm că astfel arborele oarecare se transformă într-un arbore binar.

124
Figura 1-2

Figura 1-2 indică reprezentarea arborelui oarecare de mai sus, sub forma unui arbore cu legături fiu-
frate. Legătura fiu a unui nod face legătura cu primul fiu al nodului. Legătura frate face legătura cu
următorul frate (fiu al aceluiași nod).

În implementarea prin intermediul unei structure de liste adăugarea unui nod se face ca ultim fiu al
unui nod deja existent, pentru care se indică nodul tată. Pentru identificarea nodului tată este necesară
parcurgerea arborelui. Aceasta se face cu ajutorul unei stive pentru adresele nodurilor la care nu s-a
utilizat legătura stângă.

Parcurgerea în preordine presupune utilizarea informației utile urmată de parcurgerea în inordine a


subarborilor fiilor. La parcurgerea în postordine se vizitează mai întâi în postordine subarborii fii, apoi
se utilizează informația utilă.

Se poate evita utilizarea explicită a stivei prin folosirea recursivității într-o reprezentare ca arbore
însăilat. Nodurile unui arbore însăilat conțin un câmp suplimentar cu valoarea 1, dacă legătura este una
normală, respectiv 2, dacă legătura este de însăilare. Într-o altă variantă se poate modifica semnificația
câmpului tipn așa încât să ia valorile:
1 pentru câmp de informaţie utilă în care llfiu este o legătură la lista de fii,
2 pentru câmp din lista de fii în care lnext este o legătură la următorul fiu,
3 pentru câmp de informaţie utilă în care llfiu este o legătură de însăilare,
4 pentru câmp din lista de fii în care lnext este o legătură de însăilare.

În implementarea bazată pe legături fiu-frate, în câmpurile care alcătuiesc structura nodurilor sunt data
pentru informația utilă, lfiu pentru legătura către primul fiu al nodului și lfrate pentru legătura către
următorul frate al nodului curent. Accesul la oricare dintre nodurile arborelui se face prin intermediul
rădăcinii R, și efectuând o parcurgere până la idendificarea nodului sau până când nu s-au până la

125
parcurgerea tuturor nodurilor. Principiile de creare și parcurgere a arborilor prezentate anterior se
adaptează noii modalități de stocare. Evitarea utilizării unei stive suplimentare se poate face și în acest
caz prin tehnica însăilării.

1.6 Arbori binari, arbori binari de cǎutare

Se numeşte arbore binar un arbore în care fiecare nod are cel mult doi fii, unul stâng şi unul drept, şi
dacă un nod are un singur fiu, se precizează dacă este fiu stâng sau drept. În Error! Reference source
not found. este prezentat un arbore binar în care nodurile 1, 6 și 7 au numai fiu drept, iar nodul 5 are
numai fiu stâng.

Accesul la elementele arborelui binar se face pornind de la rădăcină, a cărei adresă se memorează (în
R, pentru exemplele de mai jos). Pentru configurarea structurii unui nod vom considera:
node->inf = valoarea datelor stocate în elementul nod,
node->llink = adresa fiului stâng,
node->rlink = adresa fiului drept.

Asupra elementelor unui arbore binar pot fi impuse anumite proprietăți, obținându-se astfel arbori
binari cu proprietăți aparte. Efectuarea de operații (adăugarea/extragerea unui element, concatenarea,
etc.) asupra arborilor trebuie să păstreze proprietățile acestora și acest lucru este verificat dacă,
parcurgând arborele, ordinea nodurilor este păstrată.

Figura 1-3

Se numește arbore binar strict un arbore binar care are proprietatea că fiecare nod, cu excepția
nodurilor terminale, are exact doi descendenți.

Se numește arbore binar complet un arbore binar strict care are toate nodurile terminale pe același
nivel.

126
Un arbore binar este echilibrat pe înălțime dacă este vid, iar dacă nu e vid, atunci subarborii stâng și
drept trebuie să fie echilibrați și diferența înălțimilor lor să fie cel mult 1.

Modalități de parcurgere:
 preordine - se utilizează informaţia din rădăcină, se parcurge în preordine subarborele stâng,
apoi se parcurge în preordine subarborele drept;
 inordine - se parcurge în inordine subarborele stâng, se utilizează informaţia din rădăcină, apoi
se parcurge în inordine subarborele drept;
 postordine - se parcurge în postordine subarborele stâng, se parcurge în postordine
subarborele drept, apoi se utilizează informaţia din rădăcină.

Implementarea parcurgerilor descrise anterior se poate face atât iterativ cât și recursiv. Implementarea
iterativă necesită gestionarea prin program a unei stive suplimentare pentru memorarea nodurilor care
nu au fost încă prelucrate. Spre deosebire de aceasta, în implementarea recursivă gestionarea stivei
revine sistemului, scrierea algoritmului devenind mult mai compactă. Pentru exemplificare indicăm
implementarea recursivă și iterativă pentru parcurgerea în preordine, adaptarea lor pentru celelalte
două tipuri de parcurgeri fiind evidentă.

Implementarea recursivă a parcurgerii în preordine:

preordine( node )
{
if (node != NULL)
{
write(node->inf) //sau altă prelucrare a inf din
node
preordine(node->llink)
preordine(node->rlink)
}
}
parcurgerea arborelui făcându-se cu apelul:
preordine( R )

Implementarea iterativă a parcurgerii în preordine:


S = Φ;
if (R != NULL) R => S;
while (S != Φ)
{
x <= S;
write(x->inf)
if (x->llink != NULL) x->llink => S;
if (x->rlink != NULL) x->rlink => S;
}
unde am considerat S stivă ce conține adresele nodurilor din arbore ăn ordinea în care se vor introduce.

Pentru compactarea scrierii algoritmului am folosit notațiile:


x => S pentru introducerea unui nod în stivă

127
x <= S pentru extragerea unui nod din stivă
S = Φ pentru inițializarea stivei

Eliminarea utilizării unei stive pentru parcurgerea arborelui se face dacă, în locul uneia din legături la
unul din fii, se face trimitere către nodul care urmează conform modului de parcurgere ales pentru
arbore. O asemenea legătură se numește de însăilare, iar arborele, binar (simplu) însăilat. O
modalitate de identificare a legăturilor de însăilare constă în a adăuga încă un câmp, tiprl, structurii
nodurilor arborelui, care ia valoare 1 dacă rlink este o legătură normală şi 0 dacă rlink este o legătură
de însăilare.

Se poate imagina și o implementare cu utilizarea ambelor legături pentru însăilare, arborele obținut
fiind arbore binar dublu însăilat. Identificarea legăturilor de însăilare se poate face prin adăugarea de
câmpuri tipll și tiplr care să indice tipul legăturii stângi, respectiv, drepte. O altă modalitate este de a
utiliza un singur câmp suplimentar tipl cu valorile:
0 pentru ambele legături efective,
1 pentru legătura llink de însăilare şi rlink normală,
2 pentru llink legătură normală, iar rlink de însăilare,
3 pentru cazul utilizării ambelor legături la însăilare.

1.7 Sortarea şi cǎutarea

În cele ce urmează vom prezenta principiile celor mai populari algoritmi de sortare, precum și o în
variantă în psudocod pentru fiecare. Aplicarea lor se consideră pentru vectori de întregi aflați în
memoria internă. Cel mai adesea, în compararea diverșilor algoritmi se are în vedere complexitatea
lor. În practică această informație nu reprezintă neapărat un criteriu de decizie asupra algoritmului care
trebuie folosit pentru problema dată și, în niciun caz nu este singurul criteriu de luat în considerare.
Aspectele care sunt luate în considerare pentru evaluarea unui algoritm de sortare țin de: stabilitate
(elementele cu chei egale să nu fie reordonate), spațiu suplimentar necesar (ideal O(1)), număr maxim
de comparări (ideal O(n ln n)), număr maxim de comutări de două valori între ele (ideal O(n)),
adaptiv (ideal O(n) când datele sunt aproape sortate, sau sunt puține chei unice). Vom arăta
caracteristicile generale ale fiecăruia și vom face, de asemenea, precizări cu privire la aplicabilitatea
lor pentru sortări externe.

Pe lângă bibliografia clasică specificată la sfârșitul acestui capitol, pentru caracterul lor practic, am aut
în vedere și descrierile de la https://www.toptal.com/developers/sorting-algorithms, precum și
http://quiz.geeksforgeeks.org/ pentru exemplificările de funcționare a algoritmilor. De remarcat
prezentarea care găsește la https://en.wikipedia.org/wiki/Sorting_algorithm, succintă dar bine
întocmită, bogată în informații și însoțită de tabele comparative pentru un număr important de
algoritmi de sortare.

Concluziile ar fi următoarele: complexitatea nu este un factor de decizie în alegerea unui algoritm,


fiecare dintre aceștia având avantaje și dezavantaje, astfel că nu există unul cel mai bun, ideal pe toate
aspectele menționate, condițiile inițiale date de ordonarea datelor și de distribuție a cheilor afectând
substanțial funcționarea. Ca atare, alegerea algoritmului adecvat ține de aplicație și de caracteristicile
datelor asupra cărora trebuie să funcționeze algoritmul.

128
1.7.1 Sortarea prin numărare

Sortarea prin numărare presupune ca pentru fiecare element al vectorului v să se numere câte valori
mai mici sau egale decât acesta sunt. Acestea vor indica și poziția pe care se va afla fiecare element în
vectorul ordonat w, și se memorează într-un vector p, adică y[p[i]]=x[i].
for(i=0; i < n; i++)
p[i]=0;
for(i=1; i<n; i++)
for(j=0; j<i; j++)
if(x[i] <= x[j])
p[j]++;
else
p[i]++;
for(i=0; i<n; i++)
y[p[i]]=x[i] ;

Astfel, plecând de la vectorul 7 5 2 4 3 9, obținem:

0 1 2 3 4 5
v – inițial 7 5 2 4 3 9
p 4 3 0 2 1 5
v - final 2 3 4 5 7 9

De observat că, la aplicarea algoritmului pentru sortare externă, pentru fiecare comparație se fac 3
operații I/O, de unde rezultă un număr de operații de transfer de ordinul O(n2), ceea ce-l face de
nerecomandat pentru sortări externe.

1.7.2 Sortarea prin inserție

Principiul: în orice moment, elementele vectorului de sortat v, sunt ordonate până pe poziția i-1;
elementul de pe poziția i este inserat între acestea, după ce s-au făcut comparații succesive, cu
decalarea elementelor mai mari la dreapta, cu o unitate. O descriere în pseudocod este:
insertionSort(int v[], int n)
{
int i, j, x;
for (i = 1; i < n; i++)
{
x = v[i];
j = i-1;
while (j >= 0 && v[j] > x)
{
v[j+1] = v[j];
j = j-1;
}
v[j+1] = x;
}
}
129
Prezentăm, în tabelul de mai jos, evoluția vectorului 7 5 2 4 3 9 pentru primii trei pași ai algoritmului:

7 5 2 4 3
i j
9
5 7 2 4 3
1 0
9
2 5 2 7 4 3
1
9
2 5 7 4 3
0
9
3 2 5 4 7 3
2
9
2 4 5 7 3
1
9
2 4 5 7 3
0
9

Se observă că, pe cazul cel mai nefavorabil (v ordonat descrescator), se fac n(n–1)/2 comparații și 3
operații de acces la vectorul x pentru fiecare comparație. Daca v este uniform distribuit, atunci
numărul mediu de comparații este ((n–1)+n(n–1)/2)/2, așadar o complexitate de comparare O(n2).

Să observăm stabilitatea algoritmului, faptul că nu necesită spațiu suplimentar pentru ordonare,


precum și adaptabilitatea lui (dacă vectorul este aproape sortat, timpul este O(n)). Astfel, în ciuda
complexității sale, O(n2), el este utilizat pentru vectori de dimensiune mică, sau pentru date despre
care se știe că sunt aproape sortate.

O variantă optimizată a sortării prin inserție este sortarea prin inserție binară, în care identificarea
poziției pe care trebuie inserat elementul i printre elementele de pe pozițiile 0...i-1 se face printr-o
căutare binară.

Datorită numărului mare de accesări ale vectorului, și pe poziții aleatoare, algoritmul nu se pretează la
o aplicare în cazul sortării externe.

1.7.3 Sortarea prin metoda bulelor

Principiul: dacă două elemente alăturate nu sunt în ordinea bună se face inversarea lor, după cum
urmează :
bubbleSort(int v[], int n)
{ // n dimensiunea vectorului de sortat
int i, j, comuta;
for (i = 0; i < n-1; i++)
{
comuta = 0;
for (j = 0; j < n-i-1; j++)
if (v[j] > v[j+1])
{

130
v[j] <-> v[j+1];
comuta = 1;
}
// daca nu au mai fost elemente care sa comute
// s-a terminat ordonarea
if (!comuta)
break;
}
}

Algoritmul face ca elementul cu valoarea cea mai mare să coboare cel mai repede pe poziția sa, iar
cele ușoare să urce, precum bulele de aer într-un pahar cu apă; de unde și denumirea metodei. De
observat că algoritmul este stabil și adaptiv (O(n) când este aproape sortat), precum și faptul că nu
folosește spațiu suplimentar. Complexitatea este, însă, O(n2).

Prezentăm, în tabelul de mai jos, evoluția vectorului 7 5 2 4 3 9 pentru primii doi pași ai algoritmului:

7 5 2 4 3
i j
9
0 5 7 2 4 3
0
9
5 2 7 4 3
1
9
5 2 4 7 3
2
9
5 2 4 3 7
3
9
5 2 4 3 7
4
9
1 2 5 4 3 7
0
9
2 4 5 3 7
1
9
2 4 3 5 7
2
9
2 4 3 5 7
3
9

În [2], D. Knuth conchide prin a preciza că, “bubble sort pare să nu aibă nimic care să îl recomande, cu
excepția unui nume antrenant și a faptului că conduce la unele problem teoretice interesante”, pe care
le discută.

1.7.4 Sortarea prin interclasare

Înțelegem prin interclasare (en. merge) procesul de obținere din două sau mai multe structuri de
același tip, având aceleași proprietăți, a unei structuri cu aceleași proprietăți [1]. Avem în vedere aici
structuri liniare (vectori, liste, stive, cozi). Astfel, de exemplu, interclasarea a n liste dublu înlănțuite,

131
cu valori ordonate crescător, va duce la crearea unei liste dublu înlănțuite, ce conține toate elementele
acestora, și ele sunt ordonate crescător.

Interclasarea a doi vectori


Prezentăm ideea generală de interclasare a a doi vectori ordonați, v și w, rezultatul fiind pus într-un al
treilea vector, r.
interclasare(int v[], int w[], int r[], int lv, int lw, int lr)
{
int i=0, j=0, k=0;

//se compara pe rand valorile vectorilor v si w


//valoarea cea mai mica se trece in r
//se avanseaza in r si in vectorul din care s-a copiat
valoarea
while (i < lv && j < lw)
r[k++] = v[i] < w[j] ? v[i++] : w[j++];

//copierea elementelor ramase


while (i < lv)
r[k++] = v[i++];
while (j < lw)
r[k++] = w[j++];
}

Sortarea prin interclasare (en. merge sort) este o metoda de sortare recursivă în care:
 vectorul inițial este împărțit în doi vectori,
 pentru fiecare dintre aceștia se aplică algoritmul de sortare,
 vectorii sortați se interclasează,

Dacă st și dr sunt capătul stâng, respectiv, drept al vectorului de sortat, v, atunci algoritmul de sortare
l-am putea scrie astfel:
mergeSort(int v[], int st, int dr)
{
if (st < dr)
{
int m = st+(dr-l)/2;
mergeSort(v, st, m);
mergeSort(v, m+1, dr);

merge(v, st, m, dr);


}
}

apelul făcându-se cu :
mergeSort(v, 0, dimensiune_v - 1);

132
Funcția merge apelată face interclasarea vectorilor ordonați v[st … m] și v[m+1 … dr], subvectori ai
vectorului v[st … dr], iar rezultatul este pus tot în v. Implementarea propusă mai jos folosește un
vector suplimentar b de dimensiune m-st+1, inițializat cu v[st … m].
void merge(int v[], int st, int m, int dr)
{
int i, j, k;
if(st < dr)
{
int b[m-st+1];
for(i=st, j=0; i<=m; i++, j++)
b[j]=v[i];
i = 0; j = m+1; k = st;
while(i <= m-st && j <= dr)
v[k++] = (v[j]<b[i])?v[j++]:b[i++];
while(i <= m-st)
v[k++]=b[i++];
}
}

Să observăm stabilitatea algoritmului și faptul că spațiul suplimentar este O(n) în implementări tipice.
Astfel, sortarea prin inserție este folosită la sortarea listelor înlănțuite în O(n ln n), precum și în sortări
externe, deoarece accesul la date nu se face aleator, ci succesiv.

O aplicare a algoritmului pe vectorul: 4 3 9 5 2 7 duce la următoarea evoluție a acestuia:

Figura 1-4

133
1.7.5 Sortarea rapidă

Principiul:
 se alege un pivot (există diferite implementări ale alegerii pivotului),
 se așează pivotul pe poziția pe care o va avea în vectorul ordonat,
 elementele mai mici se pun în stânga pivotului iar cele mai mari în dreapta sa,
 vectorii stâng și drept se ordonează cu aceeași metodă.

O transpunere în pseudocod în care v este vectorul de ordonat și st, dr marginile sale, ar fi :


quickSort(int v[], int st, int dr)
{
if (st < dr)
{
int pivot = partitionare(v, st, dr);
quickSort(v, st, pivot - 1);
quickSort(v, pivot + 1, dr);
}
}

Unde partitionare(v, st, dr) returnează valoarea pivotului, după ce în prealabil pe poziția respectivă a
pus elementul care se va afla în vectorul ordonat. În exemplul de mai jos se alege ca pivot elementul
de pe ultima poziție din vector, se plasează pe poziția lui corectă, astfel încât în stânga sa sunt valorile
mai mici, respectiv în dreapta sa, valorile mai mari decât el, ale vectorului v.
partitionare (int v[], int st, int dr)
{
int pivot = v[dr];
int i = st – 1, j;
for (j = st; j < dr; j++)
if (v[j] <= pivot)
{
i++;
v[i] <-> v[j]; // interschimbare
}
v[i + 1] <-> v[dr]);
return (i + 1);
}

Deși de aceeași complexitate cu sortarea prin interclasare, pe cazul vectorilor sortarea rapidă este
preferată pentru faptul că sortarea se face fără spațiu adițional, ceea ce grăbește procesul. În cazul
listelor înlănțuite însă, situația este inversată, deoarece interclasarea fiind mai eficientă datorită
parcurgerii secvențaiale a nodurilor față de multele comutări care au loc în cazul sortării rapide și care
sunt costisitoare pentru cazul listelor.

1.7.6 Căutarea

Căutarea secvențială este modalitatea de aflare a unei valori date într-o structură liniară prin
parcurgerea acesteia element cu element. Căutarea se încheie dacă elementul a fost găsit sau dacă nu a
fost găsit dar s-a parcurs întreaga structură. Numărul mediu de comparații este (n+1)/2. În cazul în care
134
structura poate fi cu cheie multiplă (valorile elementelor se pot repeta), căutarea poate presupune și
identificarea pozițiilor pe care se găsește valoarea căutată.

Căutarea în vectori ordonați se mai numește și căutare binară, principiul fiind de înjumătățire, la
fiecare pas, a dimensiunii vectorului în care se caută, prin compararea valorii de căutat cu valoarea de
la jumătatea vectorului și, dacă nu a fost identificată, să se continue căutarea în prima jumătate dacă
cheia e mai mică, respectiv în cea de a două jumătate dacă cheia e mai mare. Complexitatea este dată
de numărul maxim de pași ai algoritmului, O(ln n).

Căutarea în arbori binari. Pentru eficientizarea căutării într-un arbore de căutare, acesta se
construiește astfel încât pentru fiecare nod, orice cheie din subarborele stâng are valoare mai mică
decât nodul și orice cheie din subarborele drept are valoare mai mare decât nodul. Un astfel de arbore
se numește arbore binar de căutare.

Parcurgerea în inordine a unui arbore binar de căutare determină prelucrarea nodurilor în ordinea
crescătoare a valorilor lor.

Căutarea unui element dat se face prin compararea valorii cu rădăcina arborelui. Dacă nu coincid,
căutarea va continua în subarborele stâng sau drept după cum valoarea căutată este mai mică sau mai
mare decât rădăcina. Căutarea continuă până când elementul căutat este găsit sau s-a ajuns pe NULL.
Complexitatea este O(ln n).

2. Înțelegerea conceptelor

1. Fie A o matrice cu 20 de linii și 17 coloane, stocată într-un vector obținut prin linearizarea linie a
acesteia. Determinați rangul elementului A(3,4) în cadrul acestui vector.
Indicație. Se numără elementele matricii A, pe linii, care se pun în vector înaintea elementului A(3,4).
Rangul său în cadrul vectorului este: 17*2+4-1=37.

2. Fie A o matrice cu 20 de linii și 17 coloane. Determinați rangul elementului A(3,4) în cadrul


vectorului obținut prin linearizare linie a matricii A știind că aceasta este:
a. superior triunghiulară,
b. superior 3-diagonală
și stocarea se face optimal.
Indicație. Nu se vor stoca în vector elementele care sunt nule pe forma matricii (deci nu cele de sub
diagonala principală, respectiv nu cele de deasupra și de dedesubtul celor trei diagonale). Pentru cazul
(a) rangul se obține din 17+16+2-1=34, iar pentru (b) din 2+3+2-1=6.

3. Se dau doi vectori ale căror elemente sunt ordonate crescător. Scrieți un algoritm de interclasarea a
acestora. Comentați eficiența algoritmului, ca timp de execuție și spațiu suplimentar necesar.
Indicație. A se vedea secțiunea despre sortarea prin inserție unde este descris și algoritmul de inserare.

4. Scrieți algoritmul de inserare a unui element x pe o poziție dată, pos, în cadrul unei liste
implementate secvențial.
Indicație. Într-o implementare secvențială, elementele listei se stochează în ordine, ca elemente ale
unui vector. Inserarea presupune eliberarea poziției dorite cu deplasarea cu o poziție la dreapta a
tuturor elementelor din listă începând de pe acea poziție. Primul element al vectorului se află pe
135
poziția 0. Considerăm că variabila MAX indică numărul actual de elemente al listei. (Poziția ultimului
element al listei din cadrul vectorului va fi MAX-1.) Faptul că lista este vidă îl indicăm prin MAX=0.

if ( pos <= MAX)


{
for (i = MAX; i >= pos ; i--)
arr[i] = arr[i-1];
arr[i] = x ;
MAX = MAX+1;
};
else overlist; // pozitia nu e in lista

5. Scrieți algoritmul de eliminare dintr-o listă implementată secvențial a elementului de pe poziția,


pos.
Indicație. Vedeți indicația de la problema 6 și țineți cont să: verificați existența unui element pe poziția
indicată, să faceți deplasările la stânga cu o unitate a elementelor ce succed elementului scos și să
actualizați dimensiunea listei.

6. Indicați algoritmii de introducere, respectiv extragere a unui element în/dintr-o coadă secvențială
în care a[0] reprezintă vârful cozii, iar P indică baza ei.
Indicație. [1], p. 48.

7. Să se reprezinte evoluţia unei stive asupra căreia se efectuează următoarele operaţii, știind că
inițial ea este vidă:
a. se introduc, în ordine: 7, 15, 23, 18;
b. se extrag trei elemente;
c. se introduc: 12, 28, 15;
d. se extrage un element.
Indicație. Principiul de funcționare al unei stive este LIFO (ultimul element care intră în stivă este
primul care iese). Astfel, într-o reprezentare pe orizontală a stivei, în care baza este elemental cel mai
din stânga iar vârful este elemental cel mai din dreapta, evoluția stivei este: 7, 15, 23, 18; 7; 7, 12, 28,
15; 7, 12, 28.

8. Să se reprezinte evoluţia unei cozi asupra căreia se efectuează următoarele operaţii, știind că inițial
ea este vidă:
a. se introduc, în ordine: 7, 15, 23, 18;
b. se extrag trei elemente;
c. se introduc: 12, 28, 15;
d. se extrage un element.
Comentați situațiile în care coada este simplu înlănțuită, dublu înlănțuită, circulară simplu înlănțuită,
respectiv circulară dublu înlănțuită.
Indicație. Principiul de funcționare al unei cozi este FIFO (primul element care intră în coadă este
primul care iese). Astfel, într-o reprezentare orizontală a cozii, în care baza este elementul cel mai din
dreapta (pe unde se adaugă elemente la coadă) iar vârful este elementul cel mai din stânga (pe unde se
extrag elemente din coadă), evoluția este următoarea: 7, 15, 23, 18; 18; 18, 12, 28, 15; 12, 28, 15.
Forma (circulară sau nu) și tipul de înlănțuire (simplă sau dublă) nu impietează asupra evoluției cozii.

9. Implementați adunarea polinoamelor de o singură variabilă folosind reprezentarea acestora ca


liste.
Indicație. Fiecare element al listei conține un termen al polinomului. Configurația unui nod este:
136
termen->coeficient = coeficientul termenului,
termen->putere = puterea termenului,
termen->next = adresa elementului următor.
Termenii polinomului apar în listă în ordinea descrescătoare a puterilor. Considerăm P și Q pointeri
către două polinoame în forma descrisă anterior. P va rămâne neschimbat iar Q va conține suma celor
două polinoame.

Q1 = Q; P1 = P;
while ((P1 != NULL) && (Q1 != NULL))
{
if (Q1->putere > P1->putere)
Q1 = Q1->next;
if (Q1->putere == P1->putere)
{
Q1->coeficient = Q1->coeficient + P1->coeficient;
Q1 = Q1->next; P1 = P1->next;
}
if (Q1->putere < P1->putere)
{
PP = P1; PP->next = Q1->next; Q1->next = PP;
Q1 = PP->next;
}
}
if (Q1 == NULL)
while (P1 != NULL)
{
Q1 = P1;
P1 = P1->next;
}

10. Cum poate fi determinat, în mod optimal, cel mai mare element al unui arbore binar de cǎutare?
Justificați.
Indicație. Datorită felului ordonării nodurilor din arborele binar de căutare, cel mai mare element este
cel mai din dreapta element, plecând din rădăcină. Astfel, determinarea lui se va face plecând din
rădăcină, pe legătura dreaptă, câtă vreme aceasta nu este NULL.

11. Care este înǎlțimea maximǎ, respectiv minimă, pe care o poate avea un arbore binar de căutare cu
nodurile: 1, 42, 35, 140, 16, 127, 2? Justificați răspunsul. Daţi câte un exemplu pentru fiecare caz.
Indicație. Înălțimea maximă se obține dacă pe fiecare nivel avem câte un singur nod; sunt 7 noduri,
deci înălțimea maximă este 6. Înălțimea minimă se obține pentru un arbore echilibrat; sunt 7 noduri,
deci înălțimea minimă este 2 (3 nivele, arbore complet echilibrat).

12. Se numește arbore binar strict un arbore binar care are proprietatea că fiecare nod, cu excepția
nodurilor terminale, are exact doi descendenți. Calculați numărul total de noduri ale unui arbore
binar strict care are n noduri terminale.
Indicație. Fie N numărul total de noduri al arborelui (terminale și neterminale), ni numărul de noduri
terminale de pe nivelul i, respectiv ki numărul de noduri neterminale de pe nivelul i. Avem relația de
recurență ki + ni = 2ki-1. Se obține N=2n-1.

13. Se numește arbore binar complet un arbore binar strict care are toate nodurile terminale pe același

137
nivel. Să se determine numărul total de noduri ale unui arbore binar complet cu n noduri
terminale.
Indicație. Arborele binar complet este un caz particular de arbore binar strict, deci se poate aplica
rezultatul (cu raționamentul) de la problema precedentă. O altă rezolvare se poate da ținând cont de
forma particulară a arborilor binar compleți rezultată din definiția lor (inducție după numărul de noduri
terminale, astfel, trecerea de la un arbore binar complet cu n noduri terminale la un arbore binar
complet cu mai multe noduri terminale, înseamnă adăugarea a încă unui nivel cu 2n frunze, deci 2n
noduri terminale).

14. Să se indice toți arborii binari de căutare ale caror noduri conțin numerele: 21, 32, 93, 46.
Indicație. Cu 4 noduri se pot construi 24 de topologii de arbori binari. Parcurgerea în inordine va
indica nodurile în ordine crescătoare dacă arborele este binar de căutare. Avem, deci, 24 de arbori
binari de căutare. Se dau câteva exemple, se indică modalitatea de determinare a tuturor configurațiilor
posibile.

15. Fie un arbore binar de căutare care conține numere cuprinse între 1 și 1000. Poate fi următoarea
secvență: 927, 220, 903, 254, 890, 258, o secvență de căutare a valorii 258 în cadrul arborelui dat?
Indicație. Valorile indicate fiind obținute prin căutare în arbore binar de căutare, și ținând cont de
algoritmul specific acestei căutări, ele ar trebui să ne conducă la reconstruirea unui fragment de arbore
binar de căutare. Se verifică acest lucru.

16. Implementați un arbore binar cu n noduri. Să se scrie o procedură de parcurgere în preordine a


arborelui, cu afișarea pe ecran a nodurilor parcurse.
Indicație. Se scriu procedurile de creare a unui nod și de parcurgere în preordine, într-una din
variantele descrise în secțiunea referitoare la arbori. Se apelează de n ori crearea nodului; se apelează
parcurgerea în preordine.

17. Cum se reprezintă într-o structură de date un arbore binar însăilat? Exemplificați pe un arbore în
care însăilarea are în vedere parcurgerea în preordine.
Indicație. A se vedea secțiunea precedentă.
18. Cum se reprezintă arborii de expresii într-o structură de date?
Indicație. Nodurile arborilor de expresii conțin operatori, respectiv operanzi (constante sau variabile).
Fiecare nod ce conține un operator are tot atâția fii cât este gradul operatorilor. Pentru evaluarea
expresiei se face evaluarea recursivă a subarborilor rădăcinii.

19. Aplicați algoritmul de sortare prin inserție vectorului 35, 7, 4, 8, 32, 6. Determinati ordinea
elementelor vectorului după efectuarea celui de al treilea pas din execuția algoritmului.
Indicație. În prezentarea teoriei din capitolul precedent se află și descrierea evoluției unui vector la
aplicarea primilor pași ai algoritmului.

20. Explicați evoluția unui vector dat la aplicarea unui algoritm de sortare dat.
Indicație. După cum se fac descrierile în partea de teorie din secțiunea precedentă, cu explicarea
principiului de funcționare al algoritmului sau cu descrierea acestuia.

21. Ce se înțelege prin căutare secvențială? Care este numărul mediu de comparări într-o căutare
secvențială?
Indicație. Căutarea secvențială înseamnă căutarea unei anumite valori într-un vector oarecare, printr-o
parcurgere a acestuia de la primul până la ultimul element, sau până când elementul dat este găsit.
Numărul mediu de comparații este (n+1)/2, pentru un vector cu n elemente (1 comparație dacă
elementul este pe prima poziție, n dacă este pe ultima poziție; sau n(n+1)/2n prin luarea în considerare

138
a tuturor variantelor de identificare a valorii căutate, adică tot (n+1)/2).

22. Descrieți un algoritm de sortare pentru o listă înlănțuită.


Indicație. Algoritmul de sortare prin inserție este ușor de adaptat într-o implementare pe liste. Pentru
performață în stabilitate se recurge la sortarea prin interclasare (en. merge sort).

23. Explicați tipurile de liste înlănțuite și modalitatea în care acestea se parcurg.


Indicație. Tipurile de liste înlănțuite sunt: simplu înlănțuite (fiecare nod are o informație utilă și o
informație de legătură către nodul următor, respectiv NULL pentru legătura ultimului nod), dublu
înlănțuite (cu excepția nodurilor extreme, fiecare nod, pe lângă informația de stocat are o legătură la
nodul următor și o legătură la nodul precedent; nodurile din capete au una din legături pe NULL),
circulare (listă înlănțuită al cărei ultim nod are legătură către primul nod). Pentru lista simplu înlănțuită
se face pe baza câmpului de legătură, pornind de la primul nod până la NULL. Traversarea listei dublu
înlănțuite se face în ambele sensuri. Parcurgerea listei circulare este similară traversării listei simplu
înlănțuite, oprirea terminându-se, însă, la întâlnirea nodului din vârf.

24. Indicați caracteristicile algoritmului de sortare cu bule și a algoritmului de sortare rapidă:


principiul de funcționare, comlpexitate, utilizare.
Indicație. Răspunsul se găsește în secțiunea dedicată descrierii algoritmilor de sortare menționați.

25. Indicați algoritmii de sortare rapidă și de sortare prin inserție. Exemplificați folosind același
vector de start.
Indicație. Se explică pas cu pas principiile celor doi algoritmi și se analizează evoluția vectorului pe
fiecare dintre aceștia, așa cum este indicat și în capitolul teoretic.

3. Aplicarea și utilizarea noțiunilor teoretice

1. Fie A o matrice cu 10 linii şi 10 coloane, stocată într-un vector obţinut prin linearizarea linie a
acesteia. Determinați:
a. Numărul de elemente ale vectorului.
b. rang(A(3,7)) în cadrul vectorului.
c. rang(A(3,7)) în cadrul vectorului, ştiind că A este superior triunghiulară şi stocarea s-a
făcut optimal.
Indicație. a. Coincide cu numărul de elemente al matricii. b. Prin numărarea elementelor de pe liniile
anterior stocate și cele de pe linia a treia, avem: 10+10+7-1=26. c. 10+9+5-1=23.

2. Fie v vectorul care stochează cifrele ce alcătuiesc CNP-ul dumneavoastră, în ordinea din CNP.
Presupunând că acesta reprezintă linearizarea linie a unei matrici superior k-diagonale, să se
determine această matrice. Indicaţi toate soluţiile posibile.
Indicație. Vectorul fiind obținut din linearizarea unei matrici superior k-diagonale, celelalte elemente
ale matricii sunt 0 și, mai mult, sunt ignorate, a.î. elementele vectorului ocupă în întregime una sau
mai multe diagonale ale matricii. Dimensiunea maximă a matricii este 13 (numărul de cifre din CNP).
Mai Dimensiunea minimă este 7 (sunt 7 elemente pe diagonala principală și 6 pe una din cele două
diagonale adiacente).

3. Scrieți algoritmul care inversează ordinea nodurilor unei liste circulare simplu înlănțuite. [3] (dar
se găsește în numeroase variante și cu diferite propuneri de rezolvare pe internet)

139
Indicație. Inversarea ordinii nodurilor presupune inversarea legăturilor dintre noduri. Accesul la listă
se face prin intermediul capului listei (head). El va fi capul listei și în lista finală. Varianta algoritmică
avută în vedere consideră un pointer x pentru parcurgerea listei, acesta fiind nodul a cărui legătură își
schimbă direcția. Se folosesc încă două variabile auxiliare, p pentru predecesorul lui x, respectiv q
pentru succesorul lui în lista inițială.

x = head->next; p = head; // initializarile pentru parcurgere


head->next = NULL; // marcarea capatului listei
while(x)
{
q = x->next; // q este succesorul lui x
x->next = p; // schimbarea sensului legăturii lui x
p = x; x = q; // deplasarea cu un nod în lista inițiala
}
head = p;

4. Indicați o modalitate de reprezentare a unei liste circulare înlănțuite astfel încât lista să poată fi
traversată eficient în ambele sensuri, fiecare celulă având un singur câmp de legătură. [2] (Vol. 1,
pr. 18). Descrieți algoritmi de inserare și ștergere a unui element.
Indicație. Câmpul de legătură al fiecărui nod conține diferența adreselor nodurilor succesor și
predecesor ale acestuia. Memorând adresele a două noduri succesive se pot deduce adresele tuturor
celorlalte elemente și, astfel, parcurgerea listei.

5. Descrieți o modalitate de implementare a mulțimilor precum și algoritmi de operare asupra


acestora (reuniune, intersecție, diferență).
Indicație. În [1], p.111, este propusă următoarea abordare. Mulțimile se memorează ca vectori ale
căror elemente sunt ordonate strict crescator. Pentru a obține reuniunea a două mulțimi se face o
parcurgere a celor doi vectori (v și w în care se stochează cele două mulțimi) ca pentru interclasarea
lor, însă, în vectorul rezultat elementele identice se iau o singură dat. Pentru operația de intersecțe se
vor pune în vectorul rezultat numai elementele identice, iar pentru diferență numai acele elemente care
sunt în primul vector dar nu și în al doilea. Pentru algoritmul de interclasare a doi vectori a se vedea
metoda indicată la secțiunea despre sortarea prin interclasare.

6. Să se afișeze în ordine inversă valorile stocate într-o listă înlănțuită.


Indicație. O soluție ar fi utilizarea unei stive suplimentare, S, în care se stochează elementele scoase pe
rând din stivă. Golirea ulterioară, cu afișare, a stivei S, face ca elementele să apară invers decât în listă.

while (L != Φ)
{
x <= L;
x => S;
}
while (S != Φ)
{
x <= S;
write(x);
}

7. Să se ordoneze crescător un șir de numere, stocat ca listă (L), folosind două stive (A, B).
140
Indicație. Stiva A este cea care va conține numerele ordonate crescător de la vârf la bază. Se scoate un
element x din listă. Se varsă în stiva B toate elementele din A care sunt mai mici decât x. Se introduce
x în A. Se varsă B în A. Se continuă până se videază L și toate elementele sunt în A. O transpunere în
pseudocod a metodei de mai sus este:

while (L != Φ)
{
x <= L;
while (VA < x)
{
y <= A;
y => B;
}
x => A;
while (B != Φ)
{
y <= B;
y => A;
}
}

8. Se dau două liste (L1, L2) ale căror elemente sunt ordonate crescător. Să se obțină o singură listă
care conține toate elementele celor două liste, ordonate crescător.
Indicație. O modalitate de rezolvare presupune utilizarea a două stive suplimentare, S 1 și S2, în care se
golesc listele L1, respectiv L2. Apoi se parcurg simultan cele două stive prin compararea vârfurilor lor.
Se extrage cel cu valoarea mai mare și se introduce în L1. Procesul se încheie după ce s-au golit
ambele stive. Lista L1 finală corespunde cerințelor problemei.

9. Se consideră o coadă cu elemente numere întregi. Scrieți algoritmul care elimină din coadă toate
numerele pare.
Indicație. Considerăm A coada inițială; folosim și o coadă auxiliară B, inițial vidă. Extragem pe rând
toate elementele din A și, pe cele care sunt impare, le punem în coada B. După vidarea cozii A, se
golește coada B în A, așa încât A să conține toate elementele impare din coada A inițială, în ordinea în
care erau.

10. Determinați câţi arbori binari se pot construi cu trei noduri distincte.
Indicație. Sunt 5 topologii posibile. Pe fiecare din acestea se valorile pot fi puse în 3! moduri
(permutări de 3 elemente). Total: 5*6=30 arbori binari.

11. Determinați câţi arbori binari de căutare se pot construi cu trei noduri distincte.
Indicație. Pe fiecare din cele 5 topologii, valorile celor trei noduri au o unică aranjare pentru ca
arborele să fie arbore binar de căutare. Răspuns: 5 arbori binari de căutare.

12. Scrieți algoritmul care permite compararea a doi arbori binari de căutare.
Indicație. Doi arbori pot să difere prin topologie și/sau prin valorile nodurilor. Doi arbori sunt egali
dacă pentru fiecare nod valorile nodurilor coincid, și sunt egali atât cei doi subarbori stângi cât și cei
drepți. Dacă r1 și r2 sunt rădăcinile celor doi arbori de comparat algoritmul de verificare egal(r1,r2)
este:

141
int compara_arbori_binari (nod *r1, nod *r2)
(
if(r1 == NULL) then return r2 == NULL;
if(r2 == NULL) then return r1 == NULL;
return r1->data == r2->data &&
egal(r1->st, r2->st) &&
egal(r1->dr, r2-> dr);
}

Funcția se apelează furnizând ca parametri adresele rădăcinilor celor doi arbori. Ea returnează 1 sau 0
după cum cei doi arbori sunt identici sau nu.

13. Se consideră arborele binar din Figura 3-1, cu f, g, h funcții binare, m, n, p funcții unare, și x,
y, z variabile. Scri eți expresia care caracterizează acest arbore binar, obținută prin parcurgerea sa
în preordine.

Figura 3-1
Indicație. fgmxnyhpzy

14. Fie f, g, h, i funcții binare, m, n funcții unare și x, y, z variabile. Reprezentați sub forma unui
arbore binar expresia f(g(h(x,y),z),m(i(x,n(z)))). Precizați tipul de parcurgere al
arborelui expresiei de mai sus și realizați implementarea acestuia.
Indicație. Se face o parcurgere în preordine a arborelui din Figura 3-2. Fiul unui nod ce
corespunde unei funcții unare este fiu stâng.

142
Figura 3-2

15. În următoare expresie aritmetică apar doar operatori binari: a+(b+c)/2*(d-e). Realizați
arborele binar asociat acesteia. Parcurgeți arborele precizând tipul parcurgerii.

Figura 3-3
Indicație. Error! Reference source not found. conține arborele binar asociat expresiei date.
Parcurgerea în: postordine abc+2/+de-* ; preordine *+a/+bc2-de; inordine a+b+c/2*d-e.

16. Scrieți algoritmul care determină optimal cel mai mare/mic element al unui arbore binar de
143
cǎutare. Justificați.
Indicație. Consecință directă a definiției arborelui binar de căutare, cel mai mare element este cel mai
din dreapta element al său. Pentru a-l determina, se parcurge arborele plecând din rădăcină și mergând
pe legătura din dreapta a fiecărui nod ș.a.m.d. până când legătura este NULL. Nodul respectiv are cea
mai mare cheie. Pentru nodul cu cheia cea mai mică se face un raționament similar pentru cel mai din
stânga nod.

17. Se organizează un campionat de tenis cu 8 jucători, pe sistemul cine pierde un meci iese din
competiție. Meciurile dintre jucătorii rămași se stabilesc prin tragere la sorți. Toate meciurile
dintre jucătorii rămași în competiție se joacă într-o singură zi. Să se determine numărul total de
meciuri și câte zile va dura campionatul. Generalizare.
Indicație. Se observă construirea unui arbore binar strict, complet de adâncime 3. Campionatul
durează 3 zile. Generalizarea presupune intrarea în competiție a unui număr de jucători n=2k, caz în
care campionatul va dura k zile.

18. Scrieți un algoritm de determinare a înălțimii unui arbore binar.


Indicație. Algoritmul se bazează pe recursivitatea definiției arborelui binar. Înălțimea fiind dată de
distanța de la rădăcină la cea mai îndepărtată frunză, vom determina înălțimea arborelui ca: 1 +
max(înălțimea_ subarborelui_ stâng, înălțimea_ subarborelui_ drept). Iar arborele vid are înălțime 0.

19. Scrieți un algoritm de determinare a numărului frunzelor unui arbore binar.


Indicație. Algoritmul se bazează pe recursivitatea definiției arborelui binar, ceea ce face ca numărul
frunzelor arborelui de rădăcină r să fie suma dintre numărul frunzelor subarborelui stâng și numărul
frunzelor subarborelui drept. Funcția care implementează algoritmul returnează numărul calculat.

int nr_frunze(nod *r)


{
if(r == NULL) return 0;
else if((r->st == NULL) && (r->dr == NULL)) return 1;
else return nr_frunze(r->st)+nr_frunze(r->dr);
}

20. Scrieți un algoritm care verifică dacă un arbore binar este echilibrat pe înălțime (arborele vid este
echilibrat, iar dacă nu e vid, subarborii stâng și drept trebuie să fie echilibrați și diferența
înălțimilor lor să fie cel mult 1).
Indicație. Funcția următoare primește ca argument adresa rădăcinii arborelui; ea returnează 1 dacă
arborele este echilibrat și 0 dacă nu e echilibrat.

int este_echilibrat(nod *r)


{
int st_h, dr_h; // inaltimile subarborelui stang, respectiv
drept
if (r == NULL) return 1; //arborele vid este echilibrat

//recuperarea inaltimii celor doi subarbori


//vezi o problema anterioara pentru algoritm
st_h = inaltime(r->st);
dr_h = inaltime(r->dr);
if ( abs(st_h-dr_h) <= 1 &&
este_echilibrat(r->st) &&
este_echilibrat(r->dr))
144
return 1;

//daca s-a ajuns aici, arborele nu este echilibrat


return 0;
}

21. Scrieți algoritmul de sortare prin inserție. Studiați evoluției vectorului cu elementele: 8, 22, 4, 7, 3,
10 la aplicarea acestui algoritm.
Indicație. A se vedea secțiunea corespunzătoare descrierii algoritmului. Evoluția vectorului: pas 1 - 8,
22, 4, 7, 3, 10; pas 2 – 4, 8, 22, 7, 3, 10; pas 3 - 4, 7, 8, 22, 3, 10; pas 4 – 3, 4, 7, 8, 22, 10; pas 5
- 3, 4, 7, 8, 10, 22.

22. Același enunț ca la problema anterioară pentru fiecare dintre algoritmii de sortare studiați.
Indicație. A se vedea secțiunea corespunzătoare descrierii fiecărui algoritm.

23. Aplicarea algoritmilor de sortare pe vector de numere naturale pentru trierea acestora în numere
pare și, respectiv, impare (la final vectorul va conține în prima parte numai numere pare, apoi
numai numere impare).
Indicație. În oricare din algoritmii de sortare, comparările cu interschimbare se finalizează dacă
elementul de indice mai mare este par și cel de indice mai mic este impar.

24. Se dă un vector sortat cu n elemente. Scrieţi algoritmul de identificare în acest vector a unui
element cu valoare precizată şi determinaţi complexitatea lui.
Indicație. Vezi secțiunea precedentă pentru descrierea metodei căutării binare.

25. Care dintre metodele de sortare următoare este mai rapidă: sortarea prin inserție (en. insertion
sort), sortarea rapidă (en. quick sort), sortarea prin metoda bulelor (en. bubble sort), sortarea
binară (en. binary sort), sortarea lineară (en. linear sort), sortarea prin combinare (en. merge sort)?
De ce? Ce ați alege pentru implementarea sortării unei liste? Comentați.
Indicație. Cea mai bună metodă de sortare este considerată a fi quick sort, atât ca timp cât și ca spațiu
suplimentar necesar, complexitatea fiind O(n log n). Criteriile care sunt de luat în considerare la
alegerea unei metode au în vedere mai multe aspecte: stabilitate (nu se reașează chei cu aceeași
valoare), spațiu suplimentar necesar, complexitate, număr maxim de comutări, comportament pe
situații extreme (datele sunt aproape ordonate, sunt puține chei unice, datele sunt aleatoare). La modul
general, nu se poate afirma că există un cel mai bun algoritm de sortare. Merge sort este bună pentru
memorie limitată. Sortarea prin selecție este mai bună decât metoda bulelor. De asemenea, ulterior
alegerii unui algoritm sau altul, trebuie avut în vedere faptul că și calitatea implementării poate aduce
îmbunătățiri asupra timpului de execuție, în cadrul aceluiași tip de sortare. De aceea, atunci când există
biblioteci care implementează o metodă de sortare, acestea sunt de preferat, pentru optimalitatea
implementării lor. O analiză comparativă mai detaliată a diverșilor algoritmi de sortare se găsește la
https://www.toptal.com/developers/sorting-algorithms.

145
Bibliografie

1. S. Bârză și L.-M. Morogan, Structuri de date, Bucureşti: Ed. FRM, 2007.


2. D. E. Knuth, Arta programării calculatoarelor, Vol.1 și 3, Ed. Teora, 2000, 2001.
3. I. Tomescu, Data Structures, Bucharest Univ. Press, 1997, 2004.
4. D. D. Burdescu și M. C. Mihăescu, Structuri de date și algoritmi. Aplicații, Craiova: Ed. Sitech,
2007.
5. T. H. Cormen, C. Leiserson și R. Rivest, Introducere în algoritmi, Cluj-Napoca: Ed. Computer
Libris Agora, 2000.

146
147
IV. Sisteme de operare
Grigore Albeanu, Prof. univ. dr., Universitatea SPIRU HARET
Alexandru Averian, Senior developer, Luxoft România

1. Organizarea structurală a sistemelor de calcul. Fundamente


Tehnica de calcul a evoluat de la abac până la supercalculatoarele actuale (sisteme hipercub, sisteme
multi-nucleu, sisteme cloud/fog ş.a.). Etapele de dezvoltare a calculatoarelor electronice sunt
cunoscute sub numele de generaţii de calculatoare. Până în prezent au fost parcurse cinci generaţii
[2]: generaţia tuburilor electronice (programe binare sau cablate, suport informaţional: cartela
perforată sau banda de hârtie perforată), generaţia tranzistorilor şi diodelor (apar suporturile
magnetice, apar limbajele de programare: FORTRAN: eng. FORmula TRANslation, COBOL: eng.
COmmon Business-Oriented Language, ALGOL: eng. ALGOrithmic Language ş.a., apar primele
sisteme de operare), generaţia circuitelor integrate (apare conceptul de firmware, apar sisteme de
operare evoluate rezidente pe disc (DOS: eng. Disk Operating System), apar noi limbaje de
programare: PL/1, Pascal, LISP: eng. LISt Processing, BASIC), generaţia VLSI: eng. Very Large
Scale Integration (apar microprocesoarele, noi tipuri de arhitecturi: microcalculatoarele (de exemplu
cele compatible IBM PC – eng. Personal Computer, staţiile MAC etc.), sisteme de operare precum
CP/M, DOS (de la IBM sau Microsoft); se dezvoltă reţelele de calculatoare; apar limbajele de
programare orientată spre obiecte) şi generaţia inteligenţei artificiale (în continuă dezvoltare) cu
trimitere către viitoarele sisteme neurale, sisteme cuantice, calcul ADN etc.

Una dintre componentele principale ale oricărui sistem de calcul o reprezintă procesorul. Procesorul
actualelor sisteme poate fi de tip special, un microprocesor sau un ansamblu integrat de
(micro)procesoare.

Orice procesor conţine patru blocuri funcţionale de bază: unitatea de comandă şi control (UCC),
unitatea aritmetico-logică (UAL), registrele procesorului, unitatea de interfaţă cu celelalte componente
(UI). Procesoarele performante utilizează structuri de date avansate precum stivele. Acestea sunt utile
pentru salvarea contextului unei activităţi înainte de întreruperea acesteia. Primele trei componente
formează unitatea de executare. UCC comandă, coordonează şi controlează întreaga activitate de
prelucrare la nivelul componentelor calculatorului. Ea va executa instrucţiunile unui program. UAL
realizează prelucrarea datelor cerută prin instrucţiuni: operaţii aritmetice, logice, de comparare etc.

Registrele reprezintă o memorie foarte rapidă a procesorului în care se păstrează codul instrucţiunilor,
datele de prelucrat, rezultatele prelucrărilor etc. Cele mai importante registre ale unui procesor sunt:
registrul acumulator, registrul numărător de adrese al programului, registrul indicatorilor de condiţii
(valoare negativă, pozitivă sau nulă, transport în urma executării operaţiilor de calcul etc.), registrul de
instrucţiuni şi registrul de adresare a memoriei. În general, registrul acumulator păstrează unul dintre
operanzii unei instrucţiuni de calcul, fiind totodată şi destinaţia rezultatului operaţiei. Registrul
numărător de adrese al programului sau registrul contor-program, arată adresa, în memoria internă,
unde se află stocată următoarea instrucţiune de executat. Indicatorii de condiţie sunt poziţionaţi
automat în urma efectuări anumitor operaţii. Registrul de instrucţiuni memorează instrucţiunea ce se
execută. Conţinutul acestui registru este analizat pentru a se determina operaţia de executat, locul unde
se află stocaţi operanzii precum şi locul unde va fi stocat rezultatul, dacă instrucţiunea este una de
calcul, respectiv adresa unde se va face un salt în program sau adresa unei zone de memorie unde/de
unde se va stoca/citi o anumită dată, în alte situaţii. Registrul de adresare a memoriei păstrează adresa

148
curentă folosită pentru efectuarea accesului la memorie. De obicei, adresa efectivă se obţine în urma
unui calcul de adresă.

UI asigură legătura dintre procesor şi celelalte componente ale calculatorului îndeplinind funcţia de
transfer de date de la/spre procesor. Comunicarea procesorului cu celelalte componente: adaptorul
video, adaptorul de disc etc. se face prin intermediul porturilor (puncte de intrare în procesor). Acestea
pot fi porturi de intrare (vin date de la componente) respectiv porturi de ieşire (pornesc date spre
componente). În practică, un port este identificat printr-un număr (unic).

Deoarece un sistem de calcul execută mai multe activităţi, acestea pot avea nevoie de controlul
procesorului. Rezultă necesitatea întreruperii unei activităţi pentru a trece la o altă activitate. Aceste
comutări sunt determinate fie prin hardware, fie prin software. Întreruperea hardware este declanşată la
apariţia unui semnal de întrerupere prin care procesorului i se cere să analizeze un anumit eveniment.
După ce au fost iniţiate de către procesor, unele activităţi se pot desfăşura fără controlul procesorului,
de exemplu modul de prelucrare DMA (eng. Direct Memory Accesss.)

Stocarea informaţiilor în sistemul de calcul se realizează prin intermediul memoriei. Memoria este
spaţiul de lucru primar al oricărui sistem de calcul. În funcţie de locul ocupat, distingem: memoria
centrală (numită şi memoria principală sau internă) şi memoria secundară (numită şi auxiliară sau
secundară). În memoria centrală sunt stocate programele şi informaţiile utilizate de ele în timpul
execuţiei lor de către procesor. Memoria secundară păstrează cantităţi mari de date şi programe
folosite frecvent şi încărcabile rapid în memoria centrală. Memoria unui sistem de calcul este
caracterizată prin: capacitate, timp de acces, viteză de transfer, cost, mod de accesare a informaţiei
stocate etc.

Totalitatea echipamentelor unui sistem de calcul diferite de unitatea centrală şi memoria internă
formează mulţimea echipamentelor periferice. Din această categorie fac parte unităţile de memorie
externă, echipamentele de intrare, echipamentele de ieşire şi echipamentele de intrare/ieşire. Alte
echipamente sunt destinate redării unor aspecte ale realităţii virtuale: mănuşa de date (eng: data
glove), casca VR (HMD: Head Mounted Display), camera VR (CAVE: Cave Automatic Virtual
Environment) etc.

Sistemele de calcul pot fi de tip numeric (digitale), analogic şi de tip hibrid. Calculatoarele numerice,
sunt cele care primesc, prelucrează şi transmit date/informaţii codificate binar. Ele fac obiectul acestei
lucrări. Sistemele de calcul analogice sunt echipamente alcătuite din amplificatoare operaţionale şi
circuite numerice independente, interconectate în vederea realizării unor operaţii de tip continuu:
integratoare, multiplicatoare etc. Mărimile corespunzătoare condiţiilor iniţiale se introduc sub forma
unor tensiuni electrice. Acestea sunt prelucrate şi se obţin noi tensiuni electrice ce vor fi vizualizate cu
ajutorul unor dispozitive speciale. Sistemele hibride sunt rezultatul cuplării unor sisteme numerice cu
sisteme analogice. Comunicarea între cele două tipuri de sisteme se realizează prin intermediul unor
dispozitive de conversie: analogic-digital; digital-analogic. Sistemele analogice nu fac obiectul acestei
lucrări.

O clasificare interesantă a sistemelor digitale a fost propusă de Flynn (1972) şi cuprinde atât sistemele
de calcul cu o singură unitate centrală, cât şi pe cele cu mai multe unităţi centrale. Clasificarea lui
Flynn consideră ca esenţiale două caracteristici: mărimea fluxului instrucţiunilor şi mărimea fluxului
datelor. Un sistem de calcul cu flux unic de instrucţiuni şi flux unic de date este numit sistem de calcul
SISD (eng. Single Instruction Single Data Stream). Toate sistemele de calcul tradiţionale (cu o singură
unitate centrală) sunt maşini SISD. Această categorie include sisteme de calcul, de la
microcalculatoarele personale până la calculatoarele multiutilizator de mare putere (eng. mainframe):
IBM/704, IBM/7040, IBM 360/40, IBM 370/135, PDP, FELIX C, microcalculatoarele IBM PC etc.
149
Următoarea categorie o reprezintă sistemele de calcul cu flux simplu de instrucţiuni, dar cu flux
multiplu de date, numite sisteme SIMD (eng. Single Instruction Multiple Data Stream). Aceste sisteme
se bazează tot pe o singură unitate centrală cu o singură unitate de comandă, dar cu N elemente de
prelucrare (UAL) şi N module de memorie ataşate acestora, toate interconectate (N2). Unitatea de
comandă emite instrucţiuni sub formă de flux unic spre toate elementele de prelucrare, în mod
simultan. La un moment dat, toate elementele de prelucrare active execută aceeaşi instrucţiune, numai
asupra datelor situate în propriul modul de memorie, deci flux multiplu de date. Din această clasă fac
parte unele supercalculatoare. Sistemele SIMD sunt, la rândul lor, de mai multe categorii: a)
matriceale - prelucrează datele în mod paralel şi le accesează prin adrese în loc de index şi valoare; b)
cu memorie asociativă - operează asupra datelor accesate asociativ (prin conţinut). În loc de adresă,
specificarea datelor se face prin valoare, cum ar fi: "mai mare decât", "mai mic decât", "între limitele",
"egal cu" etc.; c) matriceal-asociative - sunt sisteme de tip asociativ ce operează asupra tablourilor
multidimensionale (matrice şi masive de date); d) ortogonale - fiecare element procesor corespunde la
un cuvânt (32 sau 64 de biţi) de memorie şi, astfel, biţii de acelaşi rang ai tuturor cuvintelor pot fi
prelucraţi în paralel. Acest procedeu mai este numit procesare serială pe bit şi paralelă pe cuvânt.
Clasa sistemelor cu flux multiplu de instrucţiuni şi flux unic de date MISD (eng. Multiple Instructions
Single Data Stream) include acele structuri specializate ce folosesc mai multe fluxuri de instrucţiuni
executate pe acelaşi flux de date. Ultima categorie o reprezintă sistemele MIMD (eng. Multiple
Instructions, Multiple Data Stream), ce reprezintă un grup de calculatoare independente, fiecare cu
propriul context (program, date etc.). Multe dintre supercalculatoare şi toate sistemele distribuite intră
în această clasă. Sistemele MIMD pot fi divizate în două categorii: sistemele multiprocesor (cu
memorie comună) şi sisteme multicalculator. Pe de altă parte, fiecare din aceste clase se poate împărţi
în funcţie de modul de interconectare. Există două posibilităţi de interconectare: magistrală (similar
televiziunii prin cablu) şi comutaţie (similar reţelei telefonice). Se obţin astfel patru clase de sisteme
MIMD (figura 3.3): sisteme multiprocesor cu magistrală, sisteme multiprocesor comutate, sisteme
multicalculator cu magistrală (reţele de calculatoare) şi sisteme multicalculator comutate (sisteme
distribuite generale).

Calculatoarele dintr-o reţea pot fi de acelaşi tip (reţele omogene) sau de tipuri diferite (reţele
eterogene). Reţelele de calculatoare permit folosirea în comun a unor resurse fizice scumpe
(imprimante, discuri fixe de mare capacitate etc.) şi folosirea în comun a unor date. Datele care se
schimbă între sisteme de calcul se numesc documente electronice. În funcţie de aria de răspândire a
calculatoarelor dintr-o reţea, se disting următoarele tipuri de reţele:
1. Reţele locale - LAN (eng. Local Area Network): În aceste reţele, aria de răspândire nu
depăşeşte 2 km şi deservesc o societate comercială. Reţelele locale sunt formate de regulă din
calculatoarele aflate într-o clădire sau un grup de clădiri.
2. Reţele metropolitane - MAN (eng. Metropolitan Area Network): Aceste reţele acoperă
suprafaţa unui oraş.
3. Reţele globale - WAN (eng. Wide Area Network): Calculatoarele acestor reţele au o arie
geografică de răspândire foarte mare (reţele internaţionale, "Internet" etc.)
Avantajele strategice în mediul de afaceri/educaţional includ: facilitarea comunicaţiilor în cadrul unei
firme/şcoli, creşterea competitivităţii firmei/instituţiei, posibilitatea organizării resurselor în grupuri de
lucru cu efect asupra reducerii bugetelor afectate prelucrării datelor. Din punct de vedere operaţional
şi/sau tactic se remarcă: reducerea costurilor per utilizator, creşterea siguranţei serviciilor de calcul
(prin posibilitatea includerii serverelor “în oglindă“ (eng. mirror)), îmbunătăţirea administrării
software-ului, îmbunătăţirea integrităţii datelor (datele de pe server vor fi salvate în mod regulat),
îmbunătăţirea timpului de răspuns (într-o reţea necongestionată). Un utilizator al unei reţele locale poate
avea următoarele avantaje: mediul de calcul poate fi ales de către utilizator, creşte repertoriul de
aplicaţii, creşte securitatea informaţiei (sistemul de operare în reţea fiind cel care
restricţionează/permite accesul la datele reţelei), există posibilitatea instruirii on-line (prin intermediul
bazelor de date pentru instruire) etc.
150
Din punct de vedere software, accesul la blocurile de instrucţiuni specializate în comanda şi controlul
unităţilor de disc, imprimantelor şi a altor periferice este asigurat de către sistemul de operare al reţelei
(NOS- eng. Network Operating System). Un NOS este un software de reţea ce permite reţelei să
suporte capabilităţi multiproces (eng. multitasking) şi multiutilizator. De asemenea un NOS oferă
posibilităţi deosebite pentru partajarea resurselor şi pentru comunicare.

În acest moment se afl/a in continu/a dezvoltare sisteme MIMD de tip cloud. Fog computing este o
nou/a paradigm/a ce faciliteaz/a leg/atura cu sistemele cloud a componentelor ce fac partea din
Internetul lucrurilor (Internet of Things).

2. Structura sistemelor de operare

Un sistem de operare este "o colecţie organizată de programe de control şi serviciu, stocate permanent
într-o memorie principală sau auxiliară, specifice tipurilor de echipamente din componenţa unui sistem
de calcul, având ca sarcini: optimizarea utilizării resurselor, minimizarea efortului uman de
programare şi automatizarea operaţiilor manuale în cât mai mare măsură, în toate fazele de pregătire şi
execuţie a programelor". Sistemul de operare pune la dispoziţia utilizatorilor (operatori, programatori
etc.) şi o interfaţă concretizată într-un interpretor al comenzilor utilizatorului exprimate cu ajutorul
unui limbaj de comandă. Toate sistemele de operare moderne (UNIX/Linux, IOS, Windows etc.) oferă
şi o interfaţă grafică, comenzile fiind selectate din meniuri ierarhice folosind dispozitive de
interacţiune grafică sau tastatura. Totuşi, puterea limbajului de comandă nu poate fi atinsă numai cu
ajutorul elementelor grafice.

Interfaţa dintre sistemul de operare şi programele utilizatorului (numite şi lucrări - eng. jobs) este
asigurată de o colecţie de "instrucţiuni extinse" ale sistemului de operare numite apeluri sistem.
Folosind apeluri sistem (organizate în biblioteci statice (.lib) sau dinamice (.dll)), un program utilizator
poate crea, utiliza şi şterge diverse obiecte gestionate de către sistemul de operare.

Cele mai importante obiecte gestionate de un sistem de operare modern sunt procesele şi fişierele. De
asemenea, firele de executare (eng. threads) sunt obiecte specifice programării concurente (de
exemplu folosind limbajul Java), dar şi ca modalitate de implementare a proceselor Windows prin
programare multi-fir (eng. multi-thread programming).

Procesul (eng. task) reprezintă conceptul cheie al oricărui sistem de operare. Un proces este o entitate
dinamică care corespunde unui program în execuţie. Procesele sunt fie procese sistem, fie procese
utilizator. Procesele sistem sunt asociate unor module ale sistemului de operare, iar procesele utilizator
sunt asociate unor programe utilizator care pot să creeze alte procese utilizator sau să lanseze pentru
executare procese sistem. Un proces (numit proces tată) poate creea unul sau mai multe procese
(numite procese fiu sau descendenţi). În sistemele multiutilizator fiecare proces este caracterizat de
identificatorul proprietarului (utilizatorului).

Un fişier (eng. file) este un şir de caractere terminat printr-o marcă de sfârşit de fişier (EOF - eng. End
Of File). Fişierul poate fi stocat pe disc, în memorie etc. Una din funcţiile importante ale unui sistem
de operare este aceea de a “ascunde” dificultatea lucrului cu echipamentele periferice. Astfel, sistemul
de operare oferă apeluri sistem pentru lucrul cu fişiere: creare, ştergere, citire, scriere etc. Fişierele pot
fi grupate într-un catalog (eng. directory, folder) şi pot fi caracterizate de anumite atribute (proprietar,
dimensiune, data ultimului acces în scriere, coduri de protecţie etc.).

151
Modulele software pentru tratarea cererilor de intrare/ieşire de către sistemul de operare se numesc
drivere. Fiecare dispozitiv periferic are asociat un driver. În general, orice driver menţine o coadă a
cererilor de intrare/ieşire lansate de unul sau mai multe procese şi pe care le prelucrează într-o anumită
ordine (în funcţie de momentul lansării cererii sau conform unei liste a priorităţilor). Un driver este
răspunzător de satisfacerea cererilor de transfer de informaţie, de tratarea erorilor ce pot apărea la
realizarea unei operaţii fizice, ş.a. Un program utilizator poate efectua operaţii de intrare/ieşire la
nivelul unui driver, totuşi programul său nu mai este independent de dispozitiv. De aceea, pentru
operaţiile de intrare-ieşire se utilizează apelurile sistem sau diferitele proceduri specializate puse la
dispoziţie de mediile de programare.

Primele sisteme de operare realizau prelucrarea pe loturi de programe (eng. batch mode). Utilizatorul
nu comunica direct cu sistemul de calcul; acesta funcţiona sub controlul unui operator uman
specializat. Operatorul avea sarcina de a asigura resursele externe necesare unei lucrări (montarea
benzilor magnetice, pornirea şi oprirea diverselor echipamente periferice). De asemenea, operatorul
asigura şi introducerea lucrărilor în sistem. Comunicarea operaţiilor de executat, se realiza prin
intermediul unei interfeţe de tip alfanumeric ce utiliza un limbaj de comandă pentru descrierea
ordinelor adresate sistemului, precum şi pentru specificarea acţiunilor necesare tratării erorilor.
Primele sisteme de acest tip funcţionau în regim de monoprogramare, un singur program fiind încărcat
în memorie la un moment dat. Caracteristica de bază a acestui mod de prelucrare o reprezintă
imposibilitatea intervenţiei utilizatorului pentru a interacţiona cu programul său. Dintre conceptele
implementate pentru creşterea performanţelor şi mărirea eficienţei utilizării resurselor sistemelor de
calcul, un rol important l-a avut multiprogramarea. În sistemele cu multiprogramare, la un moment
dat, în memorie se află încărcate, pentru executare, mai multe procese (programe în executare), ce
concurează, pe baza unei scheme de priorităţi, pentru accesul la anumite resurse ale sistemului. Când o
resursă este retrasă unui proces (la încheierea acestuia sau la apariţia unui proces cu prioritate mai
mare), aceasta poate fi imediat alocată unui proces solicitant. Sistemele cu multiprogramare sunt din
ce în ce mai complexe. Ele au de rezolvat probleme dificile privind: alocarea optimă a resurselor,
evitarea interblocărilor, protecţia utilizatorilor (între ei) şi protecţia sistemului (în raport cu
utilizatorii).

În sistemele uniprocesor, execuţia mai multor programe în regim de multiprogramare pare simultană
din punctul de vedere al utilizatorului, dar la un moment dat, există doar un singur proces activ în
sistem. Totuşi, în sistemele multiprocesor sau multicalculator, două sau mai multe procese pot fi active
simultan, ele fiind prelucrate de procesoare diferite. Un alt mecanism important este multiprocesarea.
Acesta constă în multiprogramarea a două sau mai multe procese având un obiectiv comun. Într-un
sistem de operare multiproces (eng. multitasking) procesele pot comunica între ele şi îşi pot sincroniza
activităţile. Sistemele Microsoft bazate pe tehnologia NT, sistemele UNIX şi Linux sunt sisteme de
operare multiproces. Conceptul de memorie virtuală este, de asemenea, foarte important în contextul
sistemelor de operare moderne.

Mulţimea funcţiilor şi modul de realizare a acestora definesc caracteristicile unui sistem de operare.
Aceste caracteristici pot fi utilizate pentru a clasifica şi compara sistemele de operare. Cele mai
importante caracteristici sunt: i) modul de introducere a programelor în sistem [introducere serială,
paralelă, respectiv la distanţă]; ii) modul de planificare a proceselor [orientare pe lucrări; orientare pe
proces]; iii) numărul de programe prezente simultan în memorie [monoprogramare; multiprogramare];
iv) modul de utilizare a resurselor [alocare completă; timp real; partajare]; v) numărul de utilizatori ce
pot folosi sistemul în acelaşi timp [monoutilizator; multiutilizator].

Referitor la modul de utilizae a resurselor, când resursa partajată este timpul unităţii centrale, sistemul
de operare devine cu timp partajat (eng. time sharing). În general, sistemele de operare din această
152
clasă asigură utilizarea eficientă a resurselor sistemelor de calcul conversaţionale în care accesul la
resursele sistemului poate fi:
a) direct - pentru asigurarea unui control direct şi permanent asupra unui set de terminale pe baza
unui program utilizator; un caz particular al acestor sisteme îl reprezintă sistemele interactive
în timp real, în cadrul cărora se cere o valoare maximă prestabilită a timpului de răspuns;
b) multiplu - pentru accesul simultan, al unui număr mare de utilizatori, la resursele hardware şi
software ale sistemului; acest tip de acces apare când sunt cel puţin două terminale în sistem,
iar fiecare utilizator lucrează cu programul său într-o regiune de memorie (partiţie) diferită de
regiunile celorlalţi utilizatori, protejată printr-un mecanism software sau hardware;
c) în timp partajat (time-sharing) - în care alocarea timpului de acces se realizează pe baza unor
cuante de timp fixe sau variabile, de ordinul milisecundelor, utilizatorii având impresia că
lucrează simultan cu sistemul;
d) la distanţă - în care prelucrarea se produce de către mai multe calculatoare asupra datelor care
sunt distribuite în mai multe colecţii de date dispersate geografic (eng. distributed data bases).

Un sistem de operare, în forma cea mai simplă, apare ca o colecţie de proceduri cu funcţii precise şi cu
o interfaţă bine precizată între acestea. Serviciile (apelurile sistem) furnizate de sistemul de operare,
sunt solicitate prin încărcarea anumitor registre cu informaţia necesară sau depunerea acestei
informaţii în stivă şi apoi provocarea unei întreruperi cunoscută sub numele de apel supervizor sau
apel nucleu (eng. Kernel). Acest apel determină trecerea sistemului de calcul din modul utilizator în
modul nucleu (supervizor) şi transferă controlul sistemului de operare. Sistemul de operare analizează
parametrii apelului pentru a-l identifica, iar apoi apelează procedura de serviciu necesară. Această
descriere sugerează scheletul unui sistem de operare [1]:
 un program ce invocă o procedură de serviciu;
 bibliotecă de proceduri de serviciu ce corespund apelurilor sistem;
 bibliotecă de proceduri utilitare pentru procedurile de serviciu.

O altă posibilitate este de a privi sistemul de operare structurat pe niveluri, un nivel peste alt nivel. De
exemplu, un sistem de operare cu 6 niveluri poate cuprinde (din interior spre exterior): alocarea
procesorului şi multiprogramarea, gestiunea memoriei, comunicaţia operator-sistem, gestiunea
echipamentelor de intrare/ieşire, programele utilizator şi operatorul.

Pentru a-şi îndeplini funcţiile, majoritatea sistemelor de operare sunt structurate pe două straturi:
stratul logic şi stratul fizic. Stratul logic asigură interfaţa dintre utilizator şi sistem (prin comenzi,
instrucţiuni extinse şi mesaje). Stratul fizic este reprezentat de rutinele de tratare a întreruperilor
software. El este apropiat de hardware şi, în general, este transparent pentru utilizator.

Tendinţa în realizarea sistemelor de operare moderne este de a implementa cea mai mare parte a
funcţiilor sistemului de operare sub formă de procese utilizator. Pentru a solicita un serviciu, un proces
utilizator (numit şi proces client) emite o cerere unui proces de serviciu, care rezolvă solicitarea şi
transmite clientului răspunsul. Această comunicaţie între clienţi şi procesele de serviciu este asigurată
de nucleul sistemului de operare.

Exemple de sisteme de operare:


 MSDOS – monolitic, nestructurat
 Windows XP – arhitectura stratificată
 BSD Unix , Solaris – arhitectura modulară
 True 64 UNIX, QNX - mikrokernel
 Linux – artitectura monolitica
 Mach – microkernel

153
 Mac OS X – stratificat, modular
 Minix – microkernel

3. Gestiunea proceselor
3.1. Fundamente

Deoarece resursele unui sistem de calcul sunt, în cele mai multe cazuri, limitate cantitativ, este
imposibilă alocarea tuturor resurselor necesare unui proces la crearea acestuia. Astfel, la un moment
dat, este posibil ca un proces să nu mai poată continua. Imposibilitatea continuării apare şi atunci când
un proces trebuie să aştepte producerea unui eveniment. De exemplu, dacă un proces trebuie să
genereze date necesare altui proces, acesta din urmă va aştepta până când datele necesare vor fi
disponibile. Atunci când un proces se află în imposibilitate de a continua deoarece nu dispune de
resursele necesare, se spune că se află în starea BLOCAT. În general, un proces ajunge în această
stare atunci când din punct de vedere logic el nu mai poate continua. Totuşi, este posibil ca un proces
să fie conceptual gata pentru executare, dar să fie stopat deoarece sistemul de operare a decis alocarea
procesorului unui alt proces (cu prioritate mai mare). În această situaţie se spune că procesul este
ÎNTRERUPT. Cele două condiţii sunt complet diferite. În primul caz, suspendarea este inerentă (de
exemplu, se aşteaptă introducerea unei linii de la tastatură). A doua situaţie este de natură tehnică (nu
poate avea fiecare proces procesorul/nucleul său). Dacă procesul este în curs de executare şi deci în
mod implicit dispune de toate resursele necesare, se spune că procesul se află în starea ACTIV. În
sistemele de calcul cu un singur procesor, la un moment dat, numai un singur proces poate fi activ. În
sisteme de calcul multiprocesor pot fi active cel mult atâtea procese câte procesoare/nuclee există în
configuraţie.

Din punct de vedere practic, un proces constă dintr-un program şi contextul acestuia. Contextul
procesului cuprinde toate informaţiile externe programului, ce pot fi adresate de acesta şi care uneori
sunt strict necesare evoluţiei procesului. Aceste informaţii sunt grupate într-o structură numită teoretic,
vector de stare al procesului, iar practic bloc de control al procesului. Acest bloc de control conţine
informaţii precum: identificatorul procesului, starea procesorului (contorul program, conţinutul
registrelor, controlul întreruperilor etc.) - când procesul nu este activ, starea procesului (ACTIV,
ÎNTRERUPT, BLOCAT etc.), prioritatea procesului la obţinerea resurselor, codurile de protecţie,
tabela de translatare a memoriei virtuale, informaţii despre alte resurse (fişiere, dispozitive periferice
etc.) alocate procesului şi informaţii privind contabilizarea resurselor (timpul de utilizare al
procesorului, memoria ocupată, numărul operaţiilor de intrare-ieşire). Mulţimea câmpurilor conţinute
în tabela de procese diferă de la un sistem de operare la altul.

Crearea unui proces înseamnă: alocarea unui vector de stare, iniţializarea componentelor vectorului de
stare şi înregistrarea procesului într-o structură de date numită tabela proceselor. Astfel fiecare proces
are un descriptor unic şi anume: poziţia în tabela proceselor. Prin acest identificator sistemul de
operare şi alte procese îl pot referi pe toată durata lui de viaţă. Un proces creat la cererea altui proces
utilizator sau sistem (numit proces părinte), se numeşte proces fiu şi se spune că este un descendent al
procesului creator. Dacă două procese B şi C sunt create de acelaşi proces A, atunci B şi C se numesc
procese fraţi. Dacă un proces fiu creează un alt proces, acesta din urmă este şi descendent al părintelui
procesului care l-a creat. Mulţimea proceselor create de un proces A, a proceselor create de aceste
procese ş.a.m.d, formează descendenţa procesului A. În acest fel toate procesele sistemului alcătuiesc
o structură arborescentă.

154
Distrugerea sau ştergerea unui proces înseamnă eliberarea resurselor ce i-au fost alocate, eliberarea
memoriei corespunzătoare vectorului de stare, precum şi eliminarea acestuia din tabela proceselor. Un
proces se poate autodistruge, atunci când ajunge la sfârşitul executării sale sau, poate fi distrus la
cererea unui alt proces sistem sau utilizator.

Alte operaţii asupra proceselor sunt: semnalizarea, întârzierea, planificarea, schimbarea priorităţii,
suspendarea şi reluarea. Un proces poate fi semnalizat în legătură cu apariţia unui eveniment sau poate
fi întârziat până la apariţia unui eveniment. Pentru a obţine controlul unui procesor, procesele sunt
planificate. Schimbarea priorităţii unui proces influenţează modul de alocare a resurselor necesare
procesului. Un proces ÎNTRERUPT (respectiv BLOCAT) poate fi ÎNTRERUPT-SUSPENDAT
(respectiv BLOCAT-SUSPENDAT). Prin reluare, un proces BLOCAT-SUSPENDAT (respectiv
ÎNTRERUPT-SUSPENDAT) devine BLOCAT (respectiv ÎNTRERUPT). Tranziţiile posibile ale
unui proces pe timpul duratei sale de viaţă sunt [1]:
 ÎNTRERUPT ---> ACTIV: Această tranziţie este realizată prin operaţia de planificare în
următoarele condiţii: procesul are prioritate maximă, procesorul (sau unul dintre procesoare)
este liber şi procesul are alocate toate resursele necesare intrării în executare.
 ACTIV ---> ÎNTRERUPT: Tranziţia are loc când un proces cu prioritate mai mare decât
procesul în executare a ajuns în starea ÎNTRERUPT şi are nevoie de procesor. Operaţia ce
cauzează tranziţia este cea de planificare.
 ACTIV ---> BLOCAT: Această tranziţie are loc când procesul ACTIV aşteaptă apariţia unui
eveniment. Astfel, procesorul ocupat de procesul ACTIV devine disponibil pentru un alt
proces aflat în starea ÎNTRERUPT.
 BLOCAT ---> ÎNTRERUPT: Trecerea în starea ÎNTRERUPT are loc când evenimentul
aşteptat de procesul BLOCAT se produce. Această tranziţie se produce în urma operaţiei de
semnalizare.
 ÎNTRERUPT ---> ÎNTRERUPT-SUSPENDAT: Tranziţia este realizată când asupra unui
proces ÎNTRERUPT se execută operaţia de suspendare.
 ÎNTRERUPT-SUSPENDAT ---> ÎNTRERUPT: Asupra unui proces întrerupt şi suspendat
se efectuează operaţia de reluare.
 BLOCAT ---> BLOCAT-SUSPENDAT: Asupra unui proces ce aşteaptă producerea unui
eveniment este executată operaţia de suspendare.
 ACTIV ---> ÎNTRERUPT-SUSPENDAT: Tranziţia are loc la suspendarea unui proces
activ. Se execută operaţia de suspendare.
 BLOCAT-SUSPENDAT ---> ÎNTRERUPT-SUSPENDAT: Pentru a avea loc această
tranziţie trebuie îndeplinite următoarele condiţii:
o procesul aşteaptă producerea unui eveniment;
o procesul a fost suspendat;
o evenimentul aşteptat se produce.
Tranziţia are loc în urma operaţiei de semnalizare.
 BLOCAT-SUSPENDAT ---> BLOCAT: Tranziţia se execută asupra unui proces ce aştepta
un eveniment, iar după aceea a fost suspendat. Se execută operaţia de reluare.

Prin operatia de distrugere, procesele trec din oricare din stările ACTIV, ÎNTRERUPT, BLOCAT,
ÎNTRERUPT-SUSPENDAT şi BLOCAT-SUSPENDAT în starea numită generic INEXISTENT.
În urma operaţiei de creare, procesele trec direct în starea ÎNTRERUPT.

În general, dacă sistemul de operare păstrează o evidenţă a fiilor fiecărui proces este posibil să se
execute operaţii de tip distrugere, suspendare, reluare nu numai asupra unui proces specificat ci şi
asupra unei descendenţe a sa.

155
Exemplul 1 [Proces UNIX]. Executarea proceselor utilizator în sistemele de operare de tip UNIX se
face pe două niveluri: utilizator şi nucleu, asociate nivelurilor de funcţionare ale procesorului
sistemului de calcul. În mod utilizator, procesele îşi pot accesa numai propriile zone de instrucţiuni şi
date, pe când în mod nucleu ele pot accesa şi spaţiul de adrese al nucleului. Nucleul sistemului de
operare este parte integrantă a oricărui proces în executare. Trecerea de la un nivel la altul se
realizează prin intermediul apelurilor sistem. După un apel sistem procesul apelant va fi în mod
nucleu, iar la întoarcerea din apelul sistem, procesul revine în modul utilizator.

Relativ la stările proceselor, sistemele de operare de tip UNIX distinge următoarele stări de bază:
executare în mod utilizator, executare în mod nucleu, gata de executare (întrerupt), în aşteptare
(blocat) şi zombie. În starea întrerupt, la un moment dat se pot afla mai multe procese. Ele sunt
gestionate de către nucleu prin intermediul unui fir de aşteptare, fiind ordonate în funcţie de prioritatea
acestora. Planificatorul alege procesul cu prioritatea maximă şi îl lansează în executare. În starea
blocat, un proces poate trece benevol când aşteaptă terminarea unei operaţii de intrare/ieşire. De
asemenea, un proces este blocat în urma planificării unui proces prioritar. La un moment dat, un
proces blocat se poate afla pe disc (proces evacuat) sau în memorie. Starea zombie caracterizează
toate procesele fiu în momentul terminării lor. Intrarea din tabela de procese este eliminată numai de
către procesul părinte. Prin urmare, procesele fiu nu sunt distruse imediat după terminarea executării
lor.

Informaţiile referitoare la procese sunt memorate într-o tabela de procese, rezidentă în memorie. Cele
mai importante caracteristici ale procesului sunt: localizarea în memorie (adresa de memorie şi adresa
de evacuare), dimensiunea, starea procesului, identificatorul procesului, identificatorul utilizatorului ce
a creat procesul, descriptorii fişierelor deschise de proces, catalogul curent, catalogul rădăcină asociat
procesului etc. Informaţiile legate de fişiere, cataloage şi utilizatori, fac parte din regiunea de date
asociată procesului ce va însoţi procesul chiar şi la evacuarea acestuia pe disc.

În sistemele de operare de tip UNIX procesele sunt create folosind apelul sistem fork. Procesul
apelant se numeşte proces părinte, iar procesul nou rezultat în urma apelului se numeşte proces fiu.
Iniţial, imaginile celor două procese sunt identice, apoi ele pot avea propria lor imagine în memorie.
Astfel, dacă părintele îşi modifică o variabilă a sa, această modificare nu este vizibilă şi pentru fiu, şi
reciproc. Fişierele deschise înainte de apelul fork, sunt partajate între părinte şi fiu. Prin urmare, dacă
un fişier a fost deschis de procesul părinte înainte de apelul fork, atunci el va fi deschis şi pentru
procesul fiu.

Pe lîngă posibilitatea de creare a unui proces nou, un proces poate lansa în executare un program
conţinut într-un fişier executabil folosind unul din apelurile numite generic exec. Acestea determină,
în diverse variante, încărcarea unui program peste procesul curent şi lansarea acestuia în lucru. Prin
apelurile exec se pot lansa în executare programe care să folosească fişierele oferite de către procesul
părinte.

Operaţiile cele mai importante, efectuate de către nucleu pentru realizarea unui apel fork sunt:
 alocă o intrare nouă în tabela de procese;
 asociază un descriptor unic procesului fiu;
 realizează o copie logică a imaginii memoriei părintelui;
 întoarce în procesul părinte descriptorul fiului, iar în procesul fiu valoarea zero;
 trece procesul fiu în starea întrerupt (în memorie sau evacuat pe disc).

După crearea unui proces fiu, acesta poate executa acelaşi cod precum părintele său, sau un alt cod.
Acest lucru este posibil prin verificarea valorii returnate de apelul fork. Să presupunem că variabila

156
care reţine valoarea returnată de apelul fork este cod_fiu. Atunci secvenţa (pseudocod) următoare
descrie situaţia în care cele două procese, după apelul fork, execută coduri diferite:

int cod_fiu;
cod_fiu = fork();
if (cod_fiu == 0) {codul_fiului();}
else {codul_parintelui();}

Procesul fiu îşi poate afla propriul descriptor folosind apelul getpid. Un proces părinte care are mai
mulţi fii va cunoaşte descriptorii fiilor lui (creaţi prin apelul fork), iar la terminarea unuia dintre fii,
nucleul sistemului de operare îi va semnaliza trecerea în starea zombie a acestui fiu.

3.2. Sincronizarea proceselor

Problema generală a sincronizării constă în găsirea unui mecanism prin care un proces ACTIV să
devină BLOCAT sau să determine BLOCAREA altui proces, pentru a aştepta producerea unui
eveniment. Procesele pot interacţiona în două feluri: indirect, prin concurarea pentru aceeaşi resursă şi
direct, prin utilizarea simultană a aceleiaşi resurse şi transmiterea unor informaţii. Dacă procesele sunt
independente din punct de vedere logic, ele interacţionează indirect, controlul concurenţei fiind
asigurat de nucleu. Când procesele concurează direct, controlul concurenţei trebuie specificat la
implementarea acestora. Activitatea de coordonare a proceselor impune găsirea de soluţii eficiente
privind: determinarea proceselor, interblocarea proceselor, excluderea mutuală a proceselor precum
şi sincronizarea acestora.

Un sistem de activităţi este nedeterminat dacă rezultatele generate depind de viteza şi ordinea de
executare a activităţilor. În caz contrar se spune că sistemul de activităţi este unic determinat.
Interblocarea proceselor apare atunci când acestea aşteaptă indefinit pentru evenimente ce nu se vor
produce. Un astfel de eveniment poate fi, de exemplu, eliberarea unei resurse (fişier de date, zone de
memorie etc.). Interacţiunea proceselor conduce uneori la restricţii în care două operaţii nu pot fi
niciodată executate în acelaşi timp. Cel mai important exemplu îl constituie utilizarea în comun, pentru
scriere/citire, a aceleiaşi resurse (fişier, zonă de memorie etc.). Resursa utilizată în comun este o
resursă critică. În această situaţie este necesară excluderea mutuală, adică un mecanism prin care,
dacă un proces utilizează resursa, celelalte procese sunt blocate. Partea unui program care solicită
resursa partajată se numeşte secţiune critică.

Soluţia corectă a problemei excluderii mutuale trebuie să satisfacă următoarele condiţii:


a) exclusivitate: cel mult un proces să fie la un moment dat în secţiunea sa critică pentru aceeaşi
resursă;
b) evitarea blocajului reciproc: un proces nu trebuie să aştepte nelimitat pentru a intra în
secţiunea sa critică;
c) uniformitatea soluţiei: nu trebuie făcută nici o presupunere asupra vitezei de execuţie a
proceselor;
d) evitarea dependenţelor inutile: în afara secţiunii critice, un proces nu trebuie să suspende alte
procese.

Din punct de vedere hardware, dezactivarea întreruperilor la intrarea în secţiunea critică şi activarea
acestora la părăsirea secţiunii critice constituie o soluţie pentru rezolvarea problemei excluderii
mutuale. Această tehnică este folosită uneori de nucleul sistemului de operare, dar pentru procesele
utilizator folosirea acestei tehnici nu este recomandată. În domeniul soluţiilor software, au fost

157
propuse mai multe metode: utilizarea variabilei poartă, metoda alternării, metoda lui Peterson, metoda
semafoarelor etc.

3.2.1. Metoda variabilei poartă

Fie A şi B două procese ciclice. Metoda variabilei poartă presupune utilizarea unei variabile comune,
iniţial permiţând accesul oricărui proces în secţiunea sa critică. La intrarea în secţiunea critică procesul
va verifica dacă accesul este permis, iar în caz afirmativ va modifica valoarea variabilei pentru a indica
intrarea sa în secţiunea critică. Mai precis, textul sursă al aplicaţiei va fi:

int poarta;//variabilă comună cu valori binare:0 – deschis; 1 -


închis
poarta = 0; //iniţial accesul este permis
//procesul A //procesul B
do{}while (poarta == 1); do{}while (poarta == 1);
poarta = 1; poarta = 1;
cod_sectiune_critica_A(); cod_sectiune_critica_B();
poarta = 0; poarta = 0;

La o examinare atentă a codului se observă că este posibil ca, la un moment dat, ambele procese să
găsească variabila poarta cu valoarea deschis. Astfel, ambele procese vor fi simultan în secţiunea
critică. Deci, această soluţie nu satisface condiţia a).

3.2.2. Metoda alternării

Dacă procesele alternează în executare atunci se poate utiliza o variabilă comună care specifică
indicele procesului ce are dreptul să intre în secţiunea critică:

char are_dreptul;//variabilă comună:'A' – proces 1; 'B' – proces 2


are_dreptul = 'A'; //iniţial accesul este permis primului proces
//procesul A //procesul B
do{}while (are_dreptul == 'B'); do{}while (are_dreptul == 'A');
cod_sectiune_critica_A(); cod_sectiune_critica_B();
are_dreptul = 'B'; are_dreptul = 'A';

În acest exemplu, ordinea de execuţie este A, B, A, B, A, ... etc. Dacă procesul A este lent atunci
procesul B va răspunde şi el greu. Această soluţie nu satisface condiţia c). Ea presupune o ordine
predefinită a execuţiei celor două procese.

3.2.3. Metoda lui Peterson

O soluţie software a problemei excluderii mutuale, în care procesele nu sunt strict alternate, a fost
propusă de Peterson în 1981. Idea metodei Peterson este prezentată prin următoarea secvenţa de cod.

int indicator; //0 – primul proces; 1 – al doilea proces


int cerere[2]; //tablou cu valori logice: 0 – fals; 1- adevarat
void intrare_in_sectiunea_critica(int proces){
158
int alt_proces;
switch (proces) {
case 0: alt_proces = 1; break;
case 1: alt_proces = 0;
}
cerere[proces] = 1;
indicator = proces;
while ((indicator == proces) && (cerere[alt_proces])) do{};
}
void iesire_din_sectiunea_critica(int proces){
cerere[proces] = 0;
}

Înainte de intrarea în secţiunea critică fiecare proces apelează funcţia intrare_in_sectiunea_critica cu


parametru indicativul propriu. Procesul va intra în aşteptare până când celălalt proces a apelat
procedura iesire_din_sectiunea_critica.

3.2.4. Metoda semafoarelor

Prin tehnica anterioară, dacă un proces încearcă să intre în secţiunea sa critică şi nu o poate face, el va
consuma timpul procesorului pe perioada timpului alocat, sperând să intre în secţiunea sa critică. Acest
tip de aşteptare se numeşte aşteptare activă. Este de preferat ca un proces ce nu poate intra în
secţiunea sa critică să se autoblocheze în aşteptarea eliberării resursei critice. Astfel, timpul
procesorului poate fi mult mai bine folosit. Un instrument util în tratarea acestei probleme este
semaforul.

Un semafor boolean (sau binar) este o variabilă întreagă care poate lua numai valorile 0 sau 1. Asupra
unui semafor binar pot acţiona următoarele operaţii: iniţializare, operaţia P: determină decrementarea
cu 1 a semaforului (dacă nu este deja zero) şi operaţia V: determină incrementarea cu 1 a semaforului
(dacă nu este deja 1).

Operaţiile P şi V sunt presupuse indivizibile, adică odată ce o astfel de operaţie a fost iniţiată de un
proces, ea nu poate fi întreruptă de un alt proces înainte de a fi executată complet.

Secţiunilor critice ale unui program li se poate asocia un semafor (iniţial 1), iar un proces ce doreşte
intrarea în secţiunea critică va executa operaţia P asupra acestui semafor. Această operaţie este
posibilă dacă valoarea semaforului nu este deja zero. În caz contrar, procesul se autoblochează. Deci,
un semafor are valoarea zero când un proces este în secţiunea sa critică, altfel va avea valoarea 1. La
ieşirea din secţiunea critică, procesul va executa operaţia V care determină creşterea valorii
semaforului; dacă există un proces blocat atunci acesta este semnalizat (dacă sunt mai multe, este ales
unul la întâmplare) şi este trecut în starea ÎNTRERUPT pentru a câştiga controlul procesorului.

Operaţiile primitive P şi V pot fi implementate şi în hardware, dar în mod normal sunt implementate în
software, folosind tehnica dezactivării întreruperilor. Gestionarea proceselor ce aşteaptă trecerea de un
semafor se face cu ajutorul unei cozi de aşteptare, operaţiile de inserare în coadă şi eliminare din coadă
fiind realizate de asemenea cu întreruperile dezactivate.

Cele de mai sus pot fi descrise prin următoarea secvenţă:

int excl;
159
excl =1; //
{cod_proces();}
....
P(excl);
{cod secţiune critică}
V(excl);
....

unde P(excl) şi V(excl) sunt definite prin:

P(excl): if (excl == 1) excl=0;


else aşteaptă_în_coada_asociată_semaforului(excl),
V(excl): if (coada_este_nevidă)
scoate_un_proces_din_coadă(excl);
else excl =1.

O generalizare a semafoarului binar o constituie semaforul numărător. Acest semafor este o variabilă
întreagă luând numai valori nenegative, iar operaţiile asupra semaforului numărător sunt iniţializarea,
operaţia PN şi operaţia VN. Operaţiile PN şi VN sunt considerate indivizibile. Un semafor numărător
este iniţializat cu o valoare x mai mare de 1, iar operaţiile PN şi VN acţionează numai asupra unor
valori din domeniul 0..x. Dacă mai multe procese încearcă să execute operaţii PN sau VN asupra
aceluiaşi semafor, ordinea de executare este arbitrară. Dacă valoarea unui semafor este zero, procesul
ce va încerca să execute o operaţie PN va fi suspendat şi va aştepta ca un alt proces să execute o
operaţie VN. Această operaţie VN determină reluarea unui proces selectat arbitrar. Definiţiile
operaţiilor PN şi VN asupra unui semafor numărător N iau următoarea formă:
PN(N): if N>0 then N-- else se suspendă procesul curent;
VN(N): if există un proces suspendat la acest semafor then selectează un proces pentru
execuţie else N++;

Diferenţa esenţială între un semafor numărător şi un semafor boolean este aceea că mai multe procese
pot executa o operaţie P asupra unui proces numărător după care pot continua execuţia. Efectul unui
semafor numărător poate fi obţinut folosind două semafoare binare şi o variabilă întreagă.

Exemplul 2 [Mecanisme de sincronizare în UNIX]. Sincronizarea proceselor în sisteme de tip UNIX


poate fi controlată de sistemul de operare sau de utilizator. În primul caz, mecanismul clasic de
sincronizare este conducta (pipe). Sincronizarea controlată de utilizator se realizează în principal prin
intermediul evenimentelor. Un eveniment reprezintă modalitatea de indicare a momentului în care un
proces, anterior BLOCAT, poate trece în starea ÎNTRERUPT (gata de rulare). Blocarea proceselor se
realizează folosind funcţia sleep apelată cu un argument ce reprezintă evenimentul aşteptat. La
producerea evenimentului de deblocare, nucleul sistemului de operare, folosind funcţia wakeup, trece
toate procesele ce aşteptau producerea evenimentului în starea ÎNTRERUPT. Din toate aceste
procese numai unul singur va trece în starea ACTIV.

Un eveniment UNIX este un număr întreg, stabilit prin convenţie, cunoscut de nucleul sistemului de
operare UNIX şi asociat unei adrese. De exemplu, terminarea unui proces este un eveniment asociat
adresei părintelui procesului curent din tabela de procese. Producerea unor evenimente în sistem este
semnalată în sistemul UNIX, fie de către nucleu, prin funcţia wakeup, fie prin intermediul semnalelor.
Semnalele UNIX sunt implementate cu ajutorul unor biţi, plasaţi în tabela de procese. Aceştia pot fi
activaţi (li se atribuie valoarea 1) fie de către nucleu (la producerea unor evenimente privind resursele
sistemului de calcul), fie de către utilizator prin intermediul apelului kill. Verificarea primirii unui
semnal este realizată de nucleu la trecerea procesului din starea în executare în mod nucleu la starea în
160
executare în mod utilizator, precum şi la blocarea unui proces (atât înainte cât şi după blocare). Un
proces poate memora semnale diferite, dar nu poate păstra mai multe semnale de acelaşi tip. Cele mai
importante semnale recunoscute de sistemul de operare UNIX sunt: terminare proces; indicare excepţii
(încercare de acces în afara limitelor permise); semnale sosite de la terminal, semnale sosite de la
procese utilizator (prin funcţiile kill şi alarm); semnale pentru erori din directive etc.

La primirea unui semnal, acţiunea asociată este cea de terminare a procesului. Totuşi există
posibilitatea de tratare, de către utilizator, a anumitor semnale. Pentru aceasta se poate folosi funcţia
signal. Astfel, prin apelul signal(semnal, proc); se indică numărul unui semnal şi rutina de tratare a
semnalului. Această rutină trebuie definită de utilizator.

Un alt tip de sincronizare o reprezintă sincronizarea unui proces părinte cu fii săi. Pentru aceasta
sistemul de operare pune la dispoziţie funcţia wait ce permite blocarea unui proces până la terminarea
unuia dintre fii săi, sau până la primirea unui semnal. Dacă la apelul funcţiei wait unul dintre fii se
terminase deja, blocarea nu se va mai realiza. Funcţia primeşte ca parametru adresa unei variabile de
tip întreg în care procesul fiu va completa informaţii despre modul în care s-a terminat şi întoarce
identificatorul procesului fiu ce s-a terminat. Un proces părinte cu descendenţi ce nu apelează funcţia
wait nu poate cunoaşte modul de terminare şi momentul terminării fiilor săi. Dacă se doreşte
sincronizarea cu un anumit fiu, se repetă apelul wait (blocarea procesului) până la terminarea fiului
specificat. Indicarea fiului se realizează prin intermediul descriptorului de proces. O funcţie importantă
utilizabilă de procese este funcţia exit care provoacă terminarea procesului apelant. Prin aceasta sunt
închise toate fişierele deschise în proces şi se transmite controlul procesului părinte. Funcţia foloseşte
un parametru de tip întreg, prin intermediul căruia se transmite procesului părinte modul de terminare
a procesului.

Alte funcţii ce permit sincronizarea controlată de utilizator sunt: kill, alarm, pause şi sleep. Funcţia
kill trimite un semnal unui proces. Ea utilizează doi parametrii: identificatorul procesului şi numărul
semnalului ce va fi transmis. Prin apelul funcţiei pause, un proces se autoblochează până la primirea
unui semnal. Această funcţie nu are parametri. Un proces care utilizează funcţia alarm cere sistemului
să transmită un semnal special (SIGALRM) după un interval de timp specificat prin parametrul
apelului. Funcţia sleep este o combinaţie între pause şi alarm şi realizează suspendarea procesului ce
o apelează pentru un timp specificat prin argumentul de apel.

Unele din funcţiile prezentate sunt apeluri sistem, altele sunt funcţii din biblioteca pentru dezvoltare de
programe. Ultimele versiuni Unix/Linux conţin şi un pachet de primitive ce implementează conceptele
de semafor, regiune de memorie partajată şi coadă de mesaje.

3.3. Comunicare între procese

Mecanismele de sincronizare de mai sus asigură rezolvarea problemelor de interacţiune între procese,
prin introducerea unei secvenţe logice de executare a acestora. Aceste mecanisme nu permit
transmiterea de informaţii între procesele ce cooperează la realizarea unui obiectiv comun.
Transmiterea unui semnal nu este suficientă; mai trebuie cunoscut şi procesul care a transmis
semnalul. De asemenea, un proces ce transmite un mesaj poate fi interesat de confirmarea primirii şi
utilizării mesajului.

Un prim mecanism de comunicare se bazează pe utilizarea zonelor comune de memorie. Cum o zonă
de memorie utilizată în comun este o resursă critică, se ajunge la programarea de secţiuni critice şi
excludere mutuală. Datele comune pot fi interpretate ca mesaje transmise de un proces altui proces
care fie le aşteaptă, fie le va găsi la nevoie.
161
Cel mai utilizat mecanism de comunicare prin intermediul unei zone comune cu sincronizare este
mecanismul producător-consumator. Fie procesele A şi B ce comunică între ele prin depunerea şi
respectiv extragerea de mesaje dintr-o zonă comună numită tampon. Presupunem că zona de memorie
poate conţine maxim N mesaje de aceeaşi lungime. De asemenea, presupunem că cele două procese au
o evoluţie ciclică ce cuprinde operaţii de depunere şi extragere de mesaje. Aceste operaţii pot fi
realizate dacă sunt îndeplinite următoarele condiţii:
 la nivel de mesaj, operaţiile se vor executa în excludere mutuală;
 nu se poate depune un mesaj atunci când nu mai există spaţiu în tampon;
 nu se poate extrage un mesaj dacă zona tampon este vidă.

O propunere de rezolvare prin utilizarea metodei wakeup-sleep este:

#define N 100
int contor = 0;
void Produc(){
while (TRUE) produc(&mesaj);
if (contor == N) sleep();
inserare(mesaj);
contor++;
if (contor == 1) wakeup(Consumator);
}
Void Consumator(){
while(TRUE) if (contor == 0) sleep();
eliminare(&mesaj);
contor--;
if (contor == N - 1) wakeup(Produc);
utilizare(mesaj);
}

Analizaţi codul pentru a identifica punctele slabe ale acestei implementări.

3.4. Planificarea proceselor

Într-un sistem de calcul cu o singură unitate centrală, la un moment dat, procesorul poate executa un
singur proces. Dacă mai multe procese solicită serviciile procesorului, acestea sunt stocate în unul sau
mai multe fire de aşteptare care sunt administrate de nucleul sistemului de operare. Componenta
nucleului sistemului de operare dedicată planificării proceselor se numeşte planificator, iar strategia
utilizată se numeşte algoritm de planificare.

Pentru sistemele cu prelucrare pe loturi (sisteme batch), metoda de planificare a lucrărilor este foarte
simplă: după executarea completă a unei lucrări se continuă cu lucrarea următoare (situată, în general,
pe medii primare de stocare). Apariţia sistemelor multiutilizator, interactive, în care procesorul este
utilizat simultan de mai multe procese (este tratat ca o resursă critică), a condus la creşterea
complexităţii algoritmilor de planificare. Planificatorul poate pune la dispoziţie şi un mecanism de
planificare, dar de cele mai multe ori el implementează o anumită strategie.

Se spune că un algoritm de planificare este ideal dacă satisface următoarele criterii:


 asigură accesul la procesor al tuturor proceselor (echitabilitate);
 asigură ocuparea permanentă a procesorului (eficienţă);
162
 minimizează timpul de răspuns pentru utilizatorii în regim conversaţional;
 minimizează timpul de executare a programelor organizate în loturi;
 maximizează numărul de procese executate pe unitatea de timp.

Analizând atent criteriile de mai sus, se observă că unele dintre acestea sunt contradictorii. De
exemplu, pentru a minimiza timpul de răspuns pentru utilizatorii în regim conversaţional,
planificatorul nu trebuie să execute programele organizate în loturi. În această situaţie, utilizatorii care
au lansat în execuţie loturi de programe, nu vor fi mulţumiţi de acest algoritm pentru că el nu satisface
criteriul al patrulea.

Dificultatea planificării constă şi în faptul că orice program este un unicat. Structura programelor nu
poate fi anticipată. Unele procese pot solicita foarte multe operaţii de intrare-ieşire, pe când altele pot
utiliza procesorul pentru perioade lungi de timp. La activarea unui proces, planificatorul nu cunoaşte
momentul când acesta se va bloca (pentru o operaţie de intrare-ieşire, aşteptare la un semafor sau din
alte motive). Pentru a se evita alocarea procesorului unui singur proces pentru o perioadă foarte lungă,
sistemele de calcul au în structura lor un ceas (orologiu) care determină apariţia periodică a unei
întreruperi. La fiecare întrerupere a ceasului, sistemul de operare va stabili procesul care devine
ACTIV. Este posibil ca procesul curent să-şi continue execuţia sau să fie întrerupt. Strategia care
permite proceselor să fie întrerupte temporar se numeşte planificare prin reciclare.

În unele sisteme de calcul dedicate, precum sistemele de gestiune a bazelor de date mari, un proces
părinte poate crea mai multe procese fiu ce execută diferite operaţii, acestea fiind complet sub
controlul procesului părinte. Pentru astfel de sisteme, un algoritm de planificare stabilit la
implementarea sistemului de operare nu poate să ofere rezultate foarte bune. Situaţia se îmbunătăţeşte
dacă sistemul de operare pune la dispoziţia programatorilor sau operatorilor un mecanism de
planificare pentru a fi folosit la realizarea unei strategii proprii de planificare. Acest obiectiv este
realizat prin parametrizarea algoritmului de planificare. Parametrii planificatorului pot avea valori
implicite sau pot primi valori prin intermediul unor comenzi ale operatorului sau apeluri sistem din
programele ce intervin în planificarea proceselor.

Strategiile de alocare sunt de mai multe tipuri: a) strategii pe bază de prioritate; b) strategii pe bază de
termen; c) strategii cu un pas; d) strategii cu mai mulţi paşi.

Prioritatea este un număr asociat fiecărui proces. Valoarea priorităţii este, în general, invers
proporţională cu numărul ce o reprezintă. Prioritatea poate fi indicată în mod explicit de utilizator
printr-o comandă de control (de exemplu, nice în UNIX/Linux etc.) sau evaluată de sistem pe baza
unor indicaţii date de utilizator sau a unor informaţii de control obţinute de sistemul de operare (de
exemplu, modelul priorităţii dinamice din UNIX/Linux).

Planificarea proceselor poate fi realizată folosind unul sau mai multe fire de aşteptare. În sistemele cu
fir de aşteptare unic, la acceptarea unui nou proces în sistem, planificatorul îl introduce în firul de
aşteptare în locul ce corespunde priorităţii sale. În sistemele ce utilizează mai multe fire de aşteptare,
pentru fiecare prioritate se gestionează un fir de aşteptare. Procesele dintr-un fir sunt luate în
considerare numai după ce toate firele de aşteptare asociate priorităţilor mai mari sunt vide.

Aceste strategii pot fi implementate atât în sistemele cu timp partajat cât şi în sistemele în care
procesorul este alocat continuu unui proces pe toată durata sa de viaţă. În cazul utilizării în regim
partajat a procesorului, procesul curent poate fi întrerupt în favoarea unui nou proces. Dacă prioritatea
noului proces este mai mare decât prioritatea procesului curent atunci procesorul este retras procesului
curent, care revine în firul de aşteptare. Astfel, procesorul este alocat noului proces.

163
Uneori, o strategie bazată pe conceptul de prioritate poate fi nesatisfăcătoare. De exemplu, unele
procese cu prioritate mică pot fi întârziate foarte mult atunci când în sistem se află un număr
considerabil de procese cu prioritate mare. De asemenea, în anumite aplicaţii este strict necesară
încheierea execuţiei unor procese într-un interval de timp precizat.

Dacă se cunosc durata de execuatre a fiecărui proces şi diverse informaţii despre încărcarea sistemului
(totalitatea proceselor aflate în sistem la un moment dat), atunci procesului i se poate asocia o
prioritate variabilă ce creşte odată cu apropierea termenului limită de finalizare. Fiind dificil de
apreciat durata de executare a proceselor, strategiile bazate pe termen sunt mai puţin utilizate.

Strategiile de planificare cu un pas sunt acele strategii prin care alocarea procesorului este continuă,
adică procesorul nu este retras unui proces decât după terminarea executării acestuia.

În funcţie de organizarea firului de aşteptare distingem: strategia "primul sosit, primul servit" (FIFO),
strategia "ultimul sosit, primul servit" (LIFO) şi strategia "cel mai scurt timp de executare".

Strategia de alocare FIFO satisface cererile de planificare a proceselor în ordinea sosirii acestora. Este
utilizat un singur fir de aşteptare (o coadă). Această strategie dezavantajează procesele scurte când
acestea sunt precedate de procese lungi.

Strategia de alocare LIFO administrează un fir de aşteptare de tip stivă, astfel încât ultimul proces
intrat în sistem este lansat în executare imediat ce procesorul devine disponibil. Dacă procesele sunt
create mai des decât sunt executate, primele procese sosite în sistem pot să aştepte foarte mult pentru
obţinerea controlului procesorului.

Strategia "cel mai scurt timp de executare" utilizează durata de executare a proceselor. Această
strategie acordă o prioritate mai mare proceselor scurte. Din acest motiv, în firul de aşteptare procesele
sunt aranjate în ordinea crescătoare a timpilor de executare. Prin această strategie sunt favorizate
procesele scurte în detrimentul celor lungi. Pentru implementarea unui astfel de algoritm este necesară
cunoaşterea duratei de executare a fiecărui proces.

Strategiile în mai mulţi paşi se bazează pe întreruperea procesului curent după un interval de timp,
numit cuantă, reintroducerea acestuia în firul de aşteptare şi alocarea procesorului unui alt proces.
Acest mod de alocare a procesorului este numit alocare prin reciclare. Dacă alocarea prin reciclare
foloseşte un fir de aşteptare ce conţine procesele în starea ÎNTRERUPT, gestionat conform disciplinei
FIFO se obţine algoritmul "round-robin". Planificatorul extrage din coadă procesul ce va primi
controlul procesorului pe durata unei cuante de timp q. Dacă procesul nu se termină în cursul alocării
curente, atunci el revine în coada de aşteptare. Prin această metodă momentul alocării procesorului nu
mai depinde de timpii de executare ai proceselor create anterior. Parametrul q poate lua valori de
ordinul milisecundelor şi este stabilit în funcţie de tipul şi obiectivele sistemului de operare. Trecerea
de la un proces la altul necesită un anumit timp pentru salvarea/încărcarea registrelor, actualizarea
unor tabele şi liste de evidenţă etc. Să presupunem că această operaţie necesită 4 milisecunde, iar q =
16 milisecunde. Atunci din 100 de milisecunde, 80 de milisecunde sunt utilizate pentru execuţie
efectivă, iar 20 de milisecunde sunt utilizate pentru trecerea de la un proces la altul. Dacă q = 500,
atunci timpul efectiv de execuţie se măreşte substanţial. În concluzie, dacă cuanta q are o valoare mică,
vor avea loc prea multe treceri de la un proces la altul, micşorând eficienţa procesorului, iar dacă
cuanta q are o valoare prea mare, procesele utilizatorilor în regim conversaţional vor fi tratate
nesatisfăcător. Într-un sistem pentru conducerea proceselor industriale, q poate fi ales astfel încât toate
interacţiunile proces industrial - sistem de calcul să se încheie într-o singură cuantă. Pentru sisteme
multiutilizator, un compromis rezonabil constă în alegerea unei cuante de 100 de milisecunde.

164
Alocarea prin reciclare bazată pe mai multe fire de aşteptare este o extensie a strategiei "round-robin",
prin definirea mai multor valori pentru cuanta de timp, q 1 < q2 < ... < qn. Procesele în starea
ÎNTRERUPT sunt repartizate în n fire de aşteptare FIFO: Q1, Q2, ..., Qn, firul Qi fiind asociat cuantei
qi. Procesele care tocmai au trecut în starea ÎNTRERUPT sunt depuse mai întâi în firul Q 1.
Planificatorul alocă procesorul unui proces din firul Qi numai dacă toate firele Qj (j < i) sunt vide.
Procesul din firul Qi care a primit controlul procesorului, păstrează acest control cel mult o cuantă qi.
Dacă procesul se blochează sau este suspendat înainte de expirarea cuantei qi, el este trecut în “coada”
proceselor din starea BLOCAT sau BLOCAT-SUSPENDAT. Altfel, după expirarea cuantei qi,
procesul este trecut în firul Qi+1 şi este selectat un nou proces pentru a primi controlul procesorului.
Procesele din firul Qn sunt trecute tot în firul Qn după expirarea cuantei qn. Se observă că această
strategie favorizează lucrările scurte, prin întârzierea celor de durată mai lungă. Strategia de alocare
prin reciclare ce utilizează mai multe fire de aşteptare împreună cu strategia bazată pe priorităţi pot
conduce la algoritmi de planificare adecvaţi sistemelor interactive. O astfel de strategie este
implementată în cadrul sistemului de operare UNIX/Linux.

Sunt posibile şi alte strategii precum: strategia "cel mai puţin executat", strategia "cel care se va
termina primul" şi strategia "procesul cu cel mai scurt termen final".

Strategia "cel mai puţin executat" depinde de timpul necesar de executare, dar spre deosebire de
strategia "cel mai scurt timp de executare", aceasta acţionează dinamic urmărind timpul care a mai
rămas până la terminarea executării. Algoritmul echilibrează accesul proceselor la resursele de calcul
şi este utilizat, mai ales, în sistemele care nu pot obţine informaţii despre procese.

Strategia "cel care se va termina primul" conduce la un algoritm orientat pe timpul de executare care
selectează pentru planificare procesul cu cel mai scurt timp rămas pentru executare.

Aplicarea algoritmului presupune atât cunoaşterea apriori a duratei aproximative de execuţie cât şi
urmărirea timpului consumat în executare pentru fiecare proces. Algoritmul favorizează ieşirea din
sistem a proceselor care se apropie de final descongestionând încărcarea sistemului şi contribuind la
asigurarea unor timpi de răspuns acceptabili. Totuşi, procesele cu durată mare de executare sunt
dezavantajate.

Strategia "procesul cu cel mai scurt termen final" presupune cunoaşterea a trei elemente: momentul
sosirii, durata executării şi momentul până la care executarea procesului trebuie să fie terminată. Pe
baza acestor elemente se stabileşte ordinea de executare în funcţie de rezerva de timp pe care o are
fiecare proces faţă de termenul final. Acest algoritm este specific sistemelor de prelucrare în timp real,
la care răspunsul trebuie dat în cadrul unui interval de timp prestabilit (de exemplu, sistemele de
operare QNX (http://www.qnx.com/content/qnx/en/products/neutrino-rtos/index.html)

Strategiile prezentate până aici au presupus, mai mult sau mai puţin, că toate procesele în starea
ÎNTRERUPT sunt încărcate în memoria centrală. Dacă nu există memorie internă suficientă, este
posibil ca unele din procesele în starea ÎNTRERUPT să fie păstrate pe disc. Această situaţie conduce
la modificarea algoritmului de planificare. Cea mai bună soluţie constă în separarea algoritmului de
planificare pe două niveluri. Nivelul 1 al planificării va răspunde de evacuarea pe disc a unor procese
ce au rămas în memorie o anumită perioadă de timp şi încărcarea în memorie a proceselor care au stat
pe disc o perioadă mai lungă de timp. Planificarea propriu-zisă este realizată de nivelul 2 al planificării
pe baza uneia din strategiile de mai sus.

În sistemele de operare UNIX, procesorul este alocat de către nucleu fiecărui proces în cuante de timp
egale, de valoare fixată. Aceasta implică faptul că la un moment dat, un singur proces aflându-se în
starea întrerupt obţine controlul procesorului. După un interval de timp cel mult egal cu durata cuantei
165
de timp alocate, nucleul va trece procesul curent din starea în executare în starea gata de executare
(întrerupt) şi va aduce în starea în executare un alt proces. Executarea procesului întrerupt va fi reluată
mai târziu. Pentru a gestiona procesele, nucleul asociază fiecărui proces activ câte o prioritate,
calculată dinamic şi asociată direct cu timpul de executare consumat de procesul respectiv.
Componenta nucleului care selectează procesul ce va fi plasat în starea în executare se numeşte
dispecer. Acesta va selecta procesul cu prioritatea cea mai mare dintre cele care se află în starea gata
de executare şi care sunt încărcate în memorie. Pentru a se putea realiza planificarea pe bază de
prioritate a proceselor, fiecare intrare în tabela de procese conţine un câmp care specifică prioritatea
procesului. În funcţie de modul de executare (utilizator sau nucleu), prioritatea unui proces este
prioritate utilizator sau prioritate nucleu. Fiecare clasă de priorităţi conţine un număr finit de valori ale
acestora, iar fiecare prioritate are asociată o coadă de procese.

Nucleul calculează prioritatea unui proces (în starea specifică acestuia) pe baza următoarelor reguli
(nivelul 2 al planificării):
 atribuie o prioritate unui proces înainte ca acesta să fie trecut în starea blocat (în aşteptare),
corelând valoarea atribuită cu cauza care a produs trecerea procesului în aşteptare;
 recalculează prioritatea procesului la revenirea acestuia în starea în executare în mod
utilizator;
 rutina de tratare a întreruperilor de ceas ajustează priorităţile proceselor din secundă în
secundă şi determină o recalculare de către nucleu a acestora (Se evită astfel controlarea
îndelungată a procesorului de către un singur proces.);
 prioritatea unui proces aflat în executare în mod utilizator este funcţie de timpul recent de
folosire a procesorului.

Timpul de lucru al procesorului este controlat prin intermediul driver-ului de ceas, care îndeplineşte
următoarele funcţii:
 reprogramează ceasul de timp real al sistemului de calcul;
 programează executarea unor funcţii interne ale nucleului în funcţie de valorile unor contoare
de timp interne;
 măsoară timpul sistem;
 transmite semnalul SIGALRM către procese (ce apelează funcţia alarm);
 activează periodic procesul swapper răspunzător de transferarea unor procese între memorie şi
disc (nivelul 1 al planificării) ;
 controlează planificarea proceselor în execuţie.

Dacă sistemul de calcul este multiprocesor/multinucleu, selectarea procesului din coada de procese
aflate în starea ÎNTRERUPT se face după algoritmii de mai sus. Afectarea procesorului este însă o
problemă complexă. În continuare se presupune că toate procesoarele sistemului de calcul au aceleaşi
caracteristici şi că un astfel de procesor poate fi liber (nu execută nici o instrucţiune) sau ocupat. Un
procesor ocupat poate aştepta rezolvarea unui conflict hardware (secvenţa de declanşare a unei
întreruperi) sau poate fi activ (execută instrucţiuni obişnuite). Un procesor este considerat liber numai
la iniţializarea sistemului de operare. Dacă la un moment dat nu există nici un proces capabil să devină
ACTIV pe un anumit procesor, se lansează în executare un proces fals (o instrucţiune de aşteptare).
Când planificatorul sistemului de operare al sistemului multiprocesor primeşte controlul, în sistem
sunt prezente n1 procese în starea ÎNTRERUPT, n2 procese în starea ACTIV şi n3 procese false.
Repartizarea procesoarelor sistemului poate fi descrisă prin: m1 procesoare ocupate cu procesele
active, m2 procesoare ocupate cu procese false şi un procesor ocupat cu planificatorul. Evident n2 =
m1, n3 = m2 şi m1+m2+1 reprezintă numărul total de procesoare din structura sistemului de calcul.

166
Pentru a lansa noi procese în execuţie, planificatorul trebuie să oprească unele procesoare (pe care se
execută fie lucrări mai puţin prioritare, fie procese false). Unele procesoare nu pot fi oprite deoarece
pot avea destinaţie specială (stabilită chiar de planificator) sau aşteaptă rezolvarea unor conflicte
hardware.

4. Gestiunea memoriei interne. Memoria virtuală


4.1. Generalităţi

Memoria este unul dintre cele mai importante subsisteme ale unui sistem de calcul. Ea are rolul de a
înmagazina informaţia. Cum memoria unui sistem de calcul este o resursă limitată, se impune
gestiunea eficientă a acesteia. Se ştie (secţiunea 1) că un sistem de calcul are în componenţa sa
memorie internă şi memorie externă. Pentru obţinerea unui răspuns rapid la o cerere de citire/scriere,
memoria internă este structurată în memorie "cache" şi memorie operativă. Memoria "cache" are o
capacitate mică, dar un timp de acces foarte scurt. Ea conţine informaţiile cele mai recent utilizate de
către unitatea centrală. La orice acces, procesorul verifică dacă data solicitată se află în memoria
"cache" şi numai după aceea solicită memoria operativă. În caz afirmativ, schimbul de informaţii are
loc imediat, altfel data solicitată este căutată în memoria operativă. Memoria "cache" este prezentă la o
mare varietate de sisteme de calcul. Memoria operativă conţine programele şi datele pentru toate
procesele existente în sistem. Memoria operativă are o capacitate mare (mai mulţi GB) şi timpul de
acces este foarte scurt. Memoria externă este structurată în memorie secundară şi memorie de arhivare.
Memoria secundară extinde memoria operativă şi apare în sistemele de operare cu memorie virtuală.
Suportul fizic al memorie secundare este, în general, discul magnetic. Memoria de arhivare constă din
fişiere (sau baze de date) rezidente pe diferite suporturi magnetice (discuri, casete speciale etc.).

Componenta sistemului de operare care se ocupă cu gestiunea memoriei se numeşte manager de


memorie. Această componentă nu trebuie confundată cu o extensie a sistemului de calcul ("cuplorul"
de memorie) care asigură gestiunea unei cantităţi mai mari de memorie faţă de posibilitatea de
adresare a procesorului. Adresa reprezintă acel atribut al memoriei interne sau externe pe baza căruia
locaţiile (celulele) de memorie pot fi localizate direct, repetitiv şi univoc, în vederea prelucrării
informaţiei memorate sau stocării unei informaţii noi.

Procesul de localizare a unei zone din memoria internă sau externă se numeşte adresare. Sunt posibile
mai multe tipuri de adresare: absolută, relativă, indexată, indirectă, asociativă etc., în funcţie de tipul
adresei considerate. Prin adresare absolută, referirea zonelor de memorie se face printr-o adresă
(numită absolută) stabilită în raport cu o poziţie iniţială numită origine. Adresarea relativă înseamnă
specificarea unei adrese ce va fi translatată pentru a se obţine o adresa absolută. Procesul de translatare
foloseşte o adresă convenţională numită bază de adresare. Adresarea indexată facilitează prelucrarea
datelor stocate succesiv, în care poziţia unui element se determină pe baza unor indici. Adresa efectivă
se obţine folosind adresa de început a zonei şi indexul elementului (format pe baza indicilor). De
obicei, indexul este memorat într-un registru general sau în registre speciale numite registre index.
Prin adresare indirectă este posibil să se utilizeze o locaţie de memorie al cărei conţinut reprezintă
adresa unei alte locaţii. Adresarea indirectă poate fi realizată în cascadă, pe mai multe nivele. În cazul
adresării asociative, localizarea unei zone de memorie se realizează pe baza conţinutului acesteia.

În sistemele de operare cu multiprogramare este necesară asigurarea protecţiei memoriei. Astfel,


fiecare unitate de memorie alocată (partiţie, pagină etc.) conţine o informaţie numită cheie de
protecţie, iar fiecare entitate încărcabilă (parte a unui program, context al unui proces) dispune de o
cheie de acces. Acestor chei li se mai asociază câteva coduri speciale (acces în citire, acces în scriere

167
şi acces în executare). În principiu, protecţia memoriei este asigurată prin parcurgerea următoarelor
etape:
 La orice invocare a unei zone de memorie se compară cheia de protecţie cu cheia de acces;
dacă apare nepotrivire atunci accesul este interzis şi se emite un semnal adecvat;
 Dacă cheile coincid, atunci se compară codurile asociate cheilor; accesul este permis numai în
cazul potrivirii complete.

Mecanismul de protecţie este puternic dependent de sistemul de calcul. Aproape fiecare sistem de
calcul are propriul mecanism de protecţie.

În primele sisteme de calcul memoria era alocată în întregime unui singur program utilizator. Toate
informaţiile necesare, aflate pe un suport extern, erau încărcate în memoria principală prin operaţii de
intrare/ieşire explicite. Evoluţia aplicaţiilor spre programe de dimensiuni mari, în condiţiile unei
memorii limitate, a obligat pe realizatorii de sisteme de operare să acorde o importanţă majoră
gestiunii memoriei principale. Soluţiile oferite au condus la conceptele de partiţie, pagină, migraţie a
proceselor între diferite niveluri ale memoriei (de exemplu, între memoria operativă şi disc) precum şi
la conceptul de memorie virtuală.

Principalele funcţii ale managerului de memorie sunt: evaluarea funcţiei de translatare a adresei
(relocare), asigurarea protecţiei memoriei, organizarea şi alocarea memoriei operative, gestiunea
memoriei secundare şi asigurarea migraţiei informaţiei între memoria principală şi memoria auxiliară.

4.2. Structuri de date şi algoritmi

Înainte de a prezenta diverse strategii de alocare a memoriei operative, se vor descrie cele mai
importante sisteme de evidenţă şi alocare/eliberare a memoriei. Prin definiţie, o zonă este un ansamblu
de locaţii succesive de memorie operativă. Implementarea unui algoritm de gestiune a memoriei
necesită rezolvarea următoarelor probleme: reprezentarea spaţiului liber, stabilirea criteriilor pentru
alegerea unei zone libere şi definirea unei strategii de eliberare a zonelor ocupate şi luarea unei decizii
când nici o zonă liberă nu convine.

O zonă este identificată prin dimensiunea sa şi prin adresa primei sale locaţii. Aceste informaţii
formează descriptorul zonei. În cazul în care numărul zonelor libere variază, descriptorii lor pot fi
plasaţi în zonele libere pe care le definesc şi apoi sunt înlănţuiţi între ei. Înlănţuirea poate fi simplă sau
dublă. În cazul înlănţuirii simple primele cuvinte ale fiecărei zone libere conţin dimensiunea acesteia
(dim) şi adresa următoarei zone libere (leg).

Pentru asigurarea unei gestiuni complete, mai este necesar un indicator al primei zone libere (init) şi
de o marcă de sfârşit de lanţ, care poate fi explicită (fin), sau adresa primei zone libere (înlănţuire
simplă circulară). Dacă {adr[i]: 0 ≤ i ≤ n} este mulţimea adreselor zonelor libere, atunci
este verificată relaţia: adr[i]->leg == adr[i+1].

Înlanţuirea dublă este realizată în felul următor. În fiecare zonă liberă se plasează informaţiile legd,
legs şi dim, care au respectiv semnificaţiile: adresa următoarei zone libere (înlănţuire înainte), adresa
zonei libere anterioare (înlănţuire înapoi) şi dimensiunea. Dacă adr[i] este a i-a zonă din lanţ,
înlănţuirea dublă este caracterizată de relaţia:
adr[i]->.legd->.legs == adr[i] == adr[i]->.legs->.legd.
În fiecare zonă se poate memora şi un indicator de ocupare (ind), care specifică starea zonei
respective: ind == 0 pentru zonă liberă, ind == 1 în cazul în care zona este ocupată. Accesul la

168
zonele libere este definit de doi indicatori, unul pentru începutul şi altul pentru sfârşitul lanţului
acestor zone.

Această reprezentare a spaţiului liber reduce timpul de căutare a unei zone libere şi permite
recuperarea zonelor eliberate. De asemenea, informaţiile de gestiune pot fi memorate şi în zonele
alocate, cu condiţia ca acestea să fie protejate. Ele pot fi folosite de algoritmul de înlocuire, pentru
alegerea zonei care va fi reacoperită, atunci când nu mai există zone libere sau c|nd cele care există nu
convin.

Ordinea zonelor libere influenţează eficienţa algoritmilor de gestiune. Sunt posibile mai multe moduri
de ordonare: 1) în ordine cronologică a eliberărilor; 2) în ordine crescătoare sau descrescătoare a
adresei de început; 3) în ordine crescătoare sau descrescătoare a dimensiunii zonelor; 4) clasare în liste
diferite, în funcţie de alte criterii.

Alegerea unei metode de ordonare depinde de algoritmii utilizaţi pentru satisfacerea cererilor de
alocare şi de eliberare. În urma unei cereri de alocare, trebuie aleasă o zonă liberă potrivită. Uneori o
cerere poate fi satisfăcută folosind o zonă liberă mai mare, iar diferenţa (reziduul), dacă nu este prea
mică, se introduce în lanţul zonelor libere. În continuare se vor prezenta cele mai importante metode
de selecţie a unei zone libere pentru satisfacerea unei cereri de alocare [1]:
1. Algoritmul primei alegeri: Această metodă alege prima zonă posibilă; dacă se solicită o zonă
de n locaţii, atunci se caută în lanţul zonelor libere până când se găseşte o zonă de dimensiune
l, astfel încât l ≥ n.
2. Algoritmul celei mai bune alegeri: Prin această metodă se alege cea mai mică zonă liberă
posibilă, adică aceea care conduce la cel mai mic reziduu.

Pentru implementarea celor două metode sunt posibile mai multe variante:
 Printre zonele care satisfac criteriul de alegere, se întârzie alocarea acelor zone care
conduc la un reziduu prea mic, în raport cu dimensiunea medie a zonelor. În acest
mod se asigură refolosirea zonelor rămase.
 Se respinge alocarea zonelor disponibile, ale căror zone vecine vor fi eliberate foarte
curând. Aceste zone vor fi alocate numai în cazul în care nu există altă posibilitate.
Prin această strategie se permite crearea unor zone libere de dimensiune mare.
 Dacă alegerea unei zone disponibile produce un reziduu mai mic decât o valoare
prestabilită, atunci se va aloca întreaga zonă. Astfel se evită crearea unor goluri de
dimensiune mică, care nu vor putea satisface nici o cerere de alocare.

3. Algoritmul înjumătăţirii (metoda camarazilor): Această metodă gestionează zone de memorie


cu dimensiunea egală cu o putere a lui 2. Presupunem că memoria operativă are capacitatea
totală 2m. Dacă dimensiunea unei cereri de alocare nu este o putere a lui 2, atunci se alocă o
zonă a cărei dimensiune este o putere a lui 2, imediat superioară dimensiunii dorite. Ideea
acestei metode este de a menţine liste separate pentru fiecare dimensiune 2k (0 ≤ k ≤ m)
pentru care există cel puţin o zonă liberă. Iniţial există o singură zonă liberă de dimensiune 2 m.
Permanent, dacă este solicitată o zonă de dimensiune 2 k şi nu există nici o zonă liberă de
această dimensiune, o zonă liberă mai mare este împărţită în două părţi egale; după un număr
finit de astfel de operaţii se obţine o zonă cu dimensiunea dorită şi alocarea poate fi
satisfăcută. Când două zone vecine, de dimensiune 2k, devin libere, ele sunt imediat regrupate
pentru a obţine o singură zonă de dimensiune 2k+1. În acest scop alocatorul memoriei foloseşte
un bit de ocupare pentru a arăta starea fiecărei zone. Memoria neocupată este reprezentată prin
liste asociate fiecărei dimensiuni. De exemplu se pot utiliza liste dublu înlănţuite. Câmpurile
legd şi legs, sunt plasate chiar în zonele libere. Elementele fiecărei liste sunt accesibile prin
specificarea a doi indicatori care se referă la primul şi respectiv la ultimul element al lanţului.
169
4. Metoda numerelor lui Fibonacci: Această tehnică este asemănătoare metodei camarazilor, dar
în loc de a diviza o zonă liberă de o anumită dimensiune în două părţi egale, ea decupează o
zonă de dimensiune ai în două zone de dimensiuni ai-1 şi ai-2, unde ai, ai-1, ai-2 sunt numerele lui
Fibonacci: ai = ai-1 + ai-2, a1 = a2 = 1. La terminarea executării unui proces este necesară
eliberarea zonelor de memorie care i-au fost alocate. Pot fi întâlnite următoarele situaţii:
 Zona eliberată se află între două zone libere, prin urmare, este preferabil ca cele trei zone
libere să fie regrupate, pentru a se obţine o singură zonă liberă de dimensiune egală cu
suma dimensiunilor celor trei zone unite;
 Zona eliberată se află între o zonă liberă şi una ocupată (caz particular al primei situaţii);
 Zona eliberată se află între două zone ocupate. În acest caz, zona respectivă este adăugată
listei zonelor disponibile.

4.3. Metode elementare de gestiune a memoriei operative

În acest paragraf se vor prezenta cele mai cunoscute metode de gestiune a memoriei operative, care nu
asigură utilizarea automată a memoriei secundare. Schimbul de informaţii între memoria operativă şi
memoria secundară este realizat de programele utilizatorilor prin operaţii de intrare-ieşire explicite.

1. Alocarea integrală şi permanentă: Această metodă presupune alocarea integrală şi în mod


permanent (a memoriei) pentru un singur program utilizator. În cazul existenţei memoriei
secundare, utilizatorul trebuie să-şi gestioneze singur, în mod explicit, transferurile de
informaţii între memoria principală şi memoria secundară. Metoda prezentată are următoarele
caracteristici:
 simplitate - programatorul are acces direct la mecanismul de adresare al calculatorului şi
nu necesită o interfaţă complexă pentru comunicaţia utilizator <--> sistem;
 cost minim - atât pentru implementarea metodei, cât şi pentru utilizarea curentă a
algoritmului de alocare (în mod evident, timpul consumat pentru gestiunea memoriei este
nul).
Această metodă, deşi simplă şi uşor de implementat, este foarte limitată în facilităţi: nu
permite înlănţuirea secvenţială a mai multor lucrări, iar resursele sistemului (timpul unităţii
centrale, memoria principală etc.) nu pot fi partajate între mai mulţi utilizatori.

2. Utilizator unic şi rezident: Această metodă rezolvă problema înlănţuirii automate a lucrărilor.
Pentru aceasta, nucleul sistemului de operare are o componentă cunoscută sub numele de
supervizor (monitor). Supervizorul este rezident în memoria centrală (într-o zonă rezervată,
numită rezidenţă) pe parcursul execuţiei tuturor programelor utilizatorilor, care pot utiliza cea
mai mare parte a memoriei principale. Pentru implementarea acestei metode este necesar ca
nivelul hardware să dispună de un mecanism de protecţie a rezidenţei. În general se utilizează
un registru limită. Dacă instrucţiunea în curs de executare se află în spaţiul rezervat
supervizorului, ea poate să modifice conţinutul oricărei locaţii din memoria centrală. Dacă,
totuşi, această instrucţiune se execută în spaţiul utilizator, ea poate accesa numai memoria care
nu este rezervată rezidenţei. Singura modalitate de acces la zona rezidenţei este utilizarea
apelurilor supervizorului.

3. Utilizarea partiţiilor fixe: Această metodă este caracteristică sistemelor cu multiprogramare.


Memoria operativă este împărţită într-un număr de zone de dimensiune fixă (numite partiţii) în
care se încarcă câte un program utilizator. Administratorul sistemului de calcul (operatorul)
poate realiza această partiţionare, manual, la fiecare pornire a sistemului.

170
4. Utilizarea partiţiilor variabile: Această metodă extinde tehnica partiţiilor fixe şi permite o
exploatare economică a memoriei centrale. Spaţiul memoriei operative este divizat într-un
număr arbitrar de zone de dimensiuni variabile (numite partiţii variabile), ce corespund
dimensiunii programelor care urmează a fi executate în aceste partiţii. În funcţie de încărcarea
sistemului, nucleul sistemului de operare actualizează numărul şi dimensiunile partiţiilor. La
încheierea execuţiei unui program, acesta părăseşte memoria centrală, iar celelalte programe
sunt deplasate spre una din limitele memoriei operative pentru a obţine la cealaltă extremitate,
o singură zonă liberă (compactarea memoriei). Această zonă va fi alocată, în continuare, altor
programe care vor fi lansate în execuţie. De asemenea, hardware-ul sistemului de calcul
trebuie să utilizeze, în primul rând, un mecanism de protecţie a partiţiilor (de exemplu registre
limită) şi, în al doilea rând, un mecanism de relocare dinamică a programelor (similar utilizării
partiţiilor fixe).

4.4. Metode avansate de gestiune a memoriei operative

În această secţiune vor fi prezentate metode de gestiune a memoriei operative care se bazează pe
utilizarea (de către nucleul sistemului de operare) a memoriilor secundare pentru creşterea eficienţei
exploatării resurselor şi îmbunătăţirea timpului de răspuns al sistemului. Metodele prezentate sunt de
două categorii. Din prima categorie fac parte acele metode prin care se alocă o zonă continuă de
memorie. O alternativă la acestea o reprezintă metodele care împart memoria în segmente, pagini sau,
şi segmente şi pagini, fiecărui program fiindu-i alocate segmente sau pagini, nu neapărat vecine.

4.4.1. Alocare continuă

Metodele de gestiune a memoriei operative aparţinând primei categorii se aplică în următoarele


condiţii: 1) un program poate fi executat numai dacă este situat în întregime în memoria centrală; 2)
celelalte programe, cu care partajează resursele sistemului, nu sunt integral şi în mod permanent
rezidente în memoria principală.

1. Metoda transferului minim: Această metodă de gestiune a memoriei este înrudită cu metoda
partiţiilor variabile, dar ea nu impune programelor ocuparea de zone disjuncte în memoria
centrală. Procesul ACTIV, este încărcat începând cu prima adresă din zona operativă. Orice
proces, care ocupă locaţii din memoria operativă susceptibile de a fi reacoperite de procesul ce
urmează a fi activat, este salvat în memoria externă, de unde va fi reîncărcat în momentul în
care va fi reactivat (la trecerea din starea ÎNTRERUPT în starea ACTIV). Acest algoritm
este conceput astfel încât cantitatea informaţiilor transferate din (sau spre) memoria operativă
să fie minimă. Într-adevăr, dacă procesul ACTIV are dimensiuni mai mari decât ale celui care
urmează a fi activat, atunci se salvează numai partea reacoperită de noul proces, restul
procesului rămânând în continuare în memoria operativă. Dacă sistemul de operare este
încărcat la baza memoriei RAM, atunci pentru implementarea acestui algoritm, hardware-ul va
utiliza numai un singur registru de protecţie a limitei inferioare a procesului ACTIV.

2. Metoda suprapunerii minime: Această metodă este o variantă îmbunătăţită a metodei


transferului minim. Restricţia impusă procesului curent, de a fi încărcat de la prima adresă a
memoriei centrale disponibile, este eliminată. Astfel, se permite încărcarea proceselor în orice
zonă continuă din memoria operativă. Această zonă este aleasă astfel încât reacoperirea să fie
minimizată (eventual eliminată). Algoritmul poate fi implementat folosind următoarele idei :

171
 memoria centrală este împărţită în zone de dimensiune fixă (numite în continuare pagini);
 fiecărui program i se alocă un anumit număr de pagini succesive, astfel încât zona formată
să-l poată cuprinde;
 alocatorul poate utiliza două tabele pentru gestiunea paginilor şi minimizarea reacoperirii
lor: tabela contorilor de reacoperire şi tabela de ocupare. În tabela contorilor de
reacoperire, fiecărei pagini din memoria operativă i se asociază o intrare pentru a
înregistra numărul proceselor care utilizează această pagină. Tabela de ocupare indică, în
orice moment, pentru fiecare pagină indicatorul procesului care o ocupă.
Pentru utilizarea acestei metode, hardware-ul trebuie să fie prevăzut cu registre limită
(superioară şi inferioară) de protecţie a paginilor şi cu un mecanism de relocare a programelor.
Introducerea priorităţilor ataşate proceselor conduce la creşterea eficienţei metodei.

3. Metoda reacoperirii întârziate: La baza acestei metode stau următoarele principii [1]:
 memoria operativă este divizată în pagini, cărora hardware-ul le asigură o protecţie
individuală;
 alocatorul administrează o tabelă de ocupare, al cărei număr de intrări este egal cu
numărul paginilor din memoria centrală; fiecare intrare conţine identificatorul procesului
care ocupă pagina în momentul respectiv;
 fiecărei pagini îi este asociat un bit de modificare, poziţionat de hardware ori de câte ori
pagina respectivă a fost implicată într-o operaţie de scriere şi repus la zero de alocator în
momentul reacoperirii acestei pagini;
 nivelul hardware al sistemului de calcul este prevăzut cu un registru special, actualizat de
nucleu, care conţine în orice moment indicatorul procesului ACTIV.
Ideea algoritmului este următoarea: Iniţial, orice proces este lansat cu un număr minim de
pagini rezidente în memoria centrală. În cursul execuţiei, pe măsură ce adresele sunt generate,
componenta hardware verifică prezenţa paginii corespunzătoare în memoria principală. Dacă
pagina adresată este prezentă, atunci execuţia continuă; în caz contrar este provocată o deviere
a cărei tratare antrenează încărcarea paginii cerute. Încărcarea este făcută imediat, dacă pagina
vizată este liberă sau dacă nu a fost modificată de către alt proces în cursul ultimei utilizări.
Din contră, dacă procesul care ocupă această pagină a modificat-o în cursul execuţiei sale,
atunci alocatorul salvează pagina în cauză, în memoria secundară, înainte de reutilizarea ei.

4.4.2. Memoria virtuală

Odată cu creşterea dimensiunii programelor utilizatorilor şi a volumului de date pe care acestea le


manipulează, au apărut noi mecanisme de adresare a memoriei şi noi metode de gestiune a resurselor.
Restricţia de continuitate a unui program în memoria centrală, impusă de metodele prezentate anterior,
a fost înlăturată prin introducerea noţiunilor de segmentare şi paginare. Noţiunea de bază utilizată în
legătură cu alocarea memoriei pentru programe de dimensiune foarte mare este memoria virtuală.
Acest termen este, de regulă, asociat cu capacitatea de a adresa un spaţiu de memorie mai mare decât
cel disponibil.

Adresele generate de un program sunt numite adrese virtuale şi formează spaţiul de adrese virtual. La
sistemele de calcul fără memorie virtuală, adresele virtuale identifică adrese din memoria fizică. În
sistemele cu memorie virtuală, adresele virtuale sunt prelucrate de cuplorul de gestiune a memorie
(memory management unit - MMU) care le translatează în adrese fizice. Memoria poate fi gestionată
prin manipularea de zone de dimensiuni fixe (alocarea paginată) sau variabile (alocarea segmentată).
Se poate utiliza, de asemenea, o combinaţie a celor două metode (alocare segmentată şi paginată).

172
A. Alocarea paginată

Metodele de gestiune din această categorie, presupun că memoria operativă (iniţial de capacitate M)
este împărţită (odată pentru totdeauna) într-un anumit număr de zone de dimensiune fixă, numite
pagini de memorie fizică (pf). În acest context, descriptorii care definesc starea ocupării memoriei
operative sunt memoraţi într-un tabel ce păstrează evidenţa alocării memoriei operative. De asemenea,
instrucţiunile şi datele fiecărui program sunt împărţite în zone de lungime fixă, numite pagini virtuale
(pv). Paginile virtuale se păstrează în memoria secundară.

Fiecare proces are propria lui tabelă de pagini, care conţine adresa fizică a paginii virtuale (când
aceasta este prezentă în memorie). La încărcarea unei noi pagini virtuale, aceasta se depune într-o
pagină fizică liberă. Are loc, astfel, o proiectare a spaţiului virtual peste cel real.

PAGINI VIRTUALE
indice: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
adresă de start: 0 4 8 12 16 20 24 28 32 36 40 44 48 52 56 60
cod: 2 3 1 0 6 x x 5 x x 7 x 4 x x x
PAGINI FIZICE
indice: 0 1 2 3 4 5 6 7
adresa de start: 0 4 8 12 16 20 24 28

Exemplu: Un sistem de calcul generează adrese de 16 biţi (de la 0 la 64KB). Acestea sunt adrese
virtuale. Presupunem că el dispune doar de 32KB memorie fizică. Rezultă că, programele mai lungi de
32KB nu pot fi încărcate complet în memorie. Spaţiul de adrese virtual va fi împărţit în pagini virtuale,
iar memoria fizică în pagini fizice. Presupunem că paginile virtuale şi paginile fizice au dimensiunea
de 4KB. Vom obţine 16 pv şi 8 pf. Presupunem că tabela de pagini a procesului asociat programului
nostru este cea din figura 2.2.6 (codul ‘x’ înseamnă pagină nealocată, iar codul numeric specifică
indicele paginii fizice asociate). Astfel, toate adresele virtuale între 0 şi 4095 vor fi asociate adreselor
fizice cuprinse între 8192 şi 12287.

Transformarea adreselor virtuale în adrese reale se realizează în mod automat, iar încărcarea paginilor
virtuale în pagini fizice se face la cerere (în urma primului acces) sau printr-o operaţie de preîncărcare.

Paginaţia poate fi definită pe unul sau mai multe nivele, adică tabela de pagini asociază unei pagini
virtuale indexul paginii fizice (asociere directă) sau adresa unei tabele de indirectare (asociere
indirectă). Această tabelă poate duce la indexul paginii fizice sau la o altă tabelă de indirectare. În
primul caz paginaţia este cu două nivele, iar în celălat caz paginaţia este cu mai mult de două nivele.
Metoda paginaţiei cu mai multe nivele are avantajul de a păstra în memorie, numai grupul de pagini
necesar la un anumit moment.

Pagina fizică corespunzătoare unei pagini virtuale este determinată pe baza adresei virtuale. În cazul
paginaţiei cu mai multe nivele, adresa virtuală are mai multe câmpuri, fiecare câmp fiind folosit pentru
a specifica indicele intrării într-o anumită tabelă. Vom exemplifica această tehnică în cazul paginaţiei
cu două nivele. Presupunem că adresa virtuală are 32 de biţi, grupaţi în trei componente: IND1 (10
biţi), IND2 (10 biţi) şi DEPLASAMENT (12 biţi). Această structură este prezentată în figura 2.2.7 (a).
Figura 2.2.7 (b) prezintă tabela paginilor, organizată pe două nivele. Primul nivel este reprezentat de o
tabelă cu 1024 de intrări (T1) care corespund celor 10 biţi ai câmpului IND1. Numărul de biţi ai
fiecărei intrări din tabela T1 determină numărul tabelelor celui de-al doilea nivel. O tabelă din acest
nivel va fi notată prin T2. Conform câmpului IND1, se va alege din T1 o intrare, iar valoarea
memorată la această intrare va indica adresa unei tabele de tip T2. Câmpul IND2 al adresei virtuale va
173
conduce la intrarea corespunzătoare din tabela de tip T2, unde va fi memorată o anumită valoare.
Această valoare împreună cu câmpul DEPLASAMENT vor genera adresa paginii fizice.

Alegerea unei strategii de alocare a paginilor fizice are o mare influenţă asupra performanţelor. Dacă
la un moment dat este activ un singur proces, atunci orice deviere pagină-absentă creează un gol în
activitatea procesorului. Pentru a creşte eficienţa de exploatare a procesorului trebuiesc create mai
multe procese, adică în momentul iniţializării sistemului se încarcă un număr minim de pagini
(necesare lansării execuţiei) pentru mai multe procese. Astfel, în timp ce se încarcă o pagină cerută de
un anumit proces, procesorul poate lucra pentru alt proces care era în starea ÎNTRERUPT. Numărul
proceselor aflate în starea ÎNTRERUPT nu trebuie să depăşească o anumită limită; cu cât acest număr
este mai mare, cu atât numărul de pagini fizice alocate unui proces este mai mic, ceea ce duce la
creşterea ratei devierilor pagină-absentă.

Sistemul poate să utilizeze două metode de alocare a paginilor: preîncărcare sau pagină la cerere.
Preîncărcarea presupune aducerea paginilor în memoria operativă înainte de prima utilizare a acestora.
În schimb, în acest caz există riscul de a încărca (în absenţa unui model realist de comportare a
programelor) pagini inutile. Metoda de încărcare pagină la cerere nu încarcă o pagină în memoria
centrală decât în momentul în care se face o referinţă la aceasta. Absenţa unei pagini adresate provoacă
o deviere, a cărei tratare are drept rezultat încărcarea paginii cerute. În cazul în care nu mai există
pagini fizice disponibile pentru a satisface cererea, trebuie reacoperită o pagină rezidentă în memoria
operativă. Algoritmul care determină pagina care trebuie reacoperită se numeşte algoritm de înlocuire.

Metoda ideală ar consta în reacoperirea paginii care va fi utilizată cel mai târziu; realizarea acestei
soluţii este foarte dificilă deoarece, de obicei, nu se poate cunoaşte comportarea viitoare a unui proces.

Algoritmii de înlocuire, imaginaţi şi utilizaţi în practică, se deosebesc prin informaţiile, pe care le iau
în considerare, relative la utilizările anterioare ale paginilor fizice şi/sau comportările viitoare ale
proceselor.

Există trei clase de algoritmi de înlocuire.

1. Algoritmi care nu utilizează nici un fel de informaţii privind comportarea anterioară sau
viitoare a proceselor:
 Algoritmul RAND utilizează o regulă statică. El presupune că toate paginile au acceaşi
şansă de a fi adresate şi în consecinţă alege la întâmplare pagina ce va fi reacoperită. În
cazul în care spaţiul virtual este mult mai mare decât capacitatea memoriei operative,
eficacitatea acestui algoritm creşte; posibilitatea de a face o alegere proastă scade.
 Algoritmul FIFO înlocuieşte pagina care a rămas cel mai mult timp în memoria
operativă. Din punct de vedere al implementării, algoritmul FIFO este preferabil; este mai
uşoară implementarea unui contor ciclic decât generarea numerelor aleatoare.

2. Algoritmi care anticipează următoarele referinţe pe baza informaţiilor extrase din istoria
procesului:
 Algoritmul LRU (Least Recently Used) înlocuieşte pagina care nu a fost utilizată de cel
mai mult timp.
 Algoritmul LFU (Least Frequently Used) înlocuieşte pagina care a fost utilizată de cele
mai puţine ori. Acest algoritm foloseşte proprietatea programelor de a avea bucle; rezultă
că unele pagini sunt des utilizate, în timp ce altele sunt utilizate mai rar.
3. Algoritmi care se bazează pe cunoaşterea comportării viitoare a procesului: Algoritmul MIN,
singurul reprezentant al acestei clase, este o extensie a celor din clasa precedentă în sensul că
minimizează numărul de încărcări, dar presupune cunoscută comportarea viitoare a procesului.
174
El funcţionează astfel: dacă există pagini care nu vor mai fi utilizate, atunci se înlocuieşte una
din ele; în caz contrar se înlocuieşte pagina care va fi folosită cel mai târziu.

B. Alocare segmentată

Memoria segmentată este întâlnită numai în sisteme de calcul capabile să interpreteze partea de adresă
a unei instrucţini ca un cuplu (s, d), unde s este identificatorul (numele) unui segment, iar d este o
deplasare relativă în acest segment. Un segment este o suită de locaţii succesive de memorie, a cărui
dimensiune este variabilă, dar limitată de numărul de biţi rezervaţi părţii de deplasare din instrucţiunea
maşină. Alocarea memoriei operative prin zone continue de dimensiune variabilă, impune existenţa
unui mecanism de adresare pentru a asigura relocarea dinamică a segmentelor. În acest context,
unitatea de alocare este segmentul.

Într-un sistem de calcul cu memorie segmentată, un program poate fi privit ca o mulţime de segmente
de cod şi de date. Mai mult, fiecare proces are o tabelă de segmente şi fiecare intrare din această tabelă
conţine adresa de început a segmentului. Fiecare segment are o structură proprie care este aplicată,
prin funcţia de asociere a adreselor, în memoria virtuală (spaţiul segmentelor), pe durata sa de
existenţă. Alocatorul realizează proiecţia spaţiului segmentelor în memoria fizică, printr-o funcţie de
translatare. Întrebarea care se pune este: cum şi în ce moment trebuie realizată translatarea? Soluţia
ideală este de a realiza translatarea adreselor în momentul execuţiei procesului. În momentul în care
este utilizată, orice adresă virtuală este tradusă prin funcţia de translatare într-o adresă fizică. Funcţia
de translatare furnizează adresa reală solicitată sau semnalează faptul că adresei virtuale nu i s-a
asociat încă o adresă a spaţiului real. Acest mod de translatare a adreselor impune relocarea dinamică a
segmentelor, cu implicaţii asupra eficienţei exploatării memoriei centrale.

Pentru realizarea relocării dinamice a segmentelor, hardware-ul trebuie să dispună de un mecanism de


adresare particular, obţinut prin utilizarea registrelor de bază sau a paginaţiei. Relocarea dinamică cu
registre de bază menţine continuitatea fizică a segmentului relocat, spre deosebire de tehnica
paginaţiei, care elimină această restricţie. Principiul relocării dinamice cu registre de bază este
următorul: Nucleul sistemului de operare utilizează un registru particular, numit registru de bază.
Conţinutul acestuia se adaugă automat fiecărei adrese generate de procesor, iar rezultatul este utilizat
pentru adresarea memoriei operative. Astfel, un segment poate fi încărcat în orice secvenţă de locaţii.
Dacă registrul de bază conţine adresa de fixare a unui segment în memoria principală, acesta va fi
utilizat în mod corect, cu condiţia ca adresa sa virtuală, de origine, să fie zero. După încărcarea unui
segment în memoria centrală, acesta poate fi deplasat, dar reutilizarea este permisă numai după ce
nucleul sistemului de operare a actualizat registrul de bază cu noua sa adresă de început.
Folosirea mai multor registre de bază distincte, pentru segmentele de cod şi de date, permite fixarea lor
independentă, ceea ce uşurează rezolvarea problemelor legate de partajarea segmentelor.

Existenţa unui mecanism de relocare dinamică (obţinut prin utilizarea registrelor de bază) facilizează
utilizarea unei strategii de alocare la cerere a zonelor de memorie operativă de dimensiune variabilă.
Este evident că, metodele de alocare cu zone de dimensiuni variabile introduc o fragmentare externă,
manifestată, după un anumit timp, prin prezenţa a numeroase zone libere, de dimensiuni prea mici
pentru a satisface cererea în curs, dar a căror dimensiune totală o depăşeşte pe cea a cererii. Pentru a
elimina această situaţie, zonele alocate pot fi compactate la o extremitate a memoriei centrale. Astfel
se va obţine (la cealaltă extremitate) o zonă liberă a cărei dimensiune este egală cu suma dimensiunilor
zonelor libere existente. Prin această strategie este posibil ca după o compactare, la următoarea cerere
situaţia să se repete. De asemenea, compactarea memoriei centrale poate fi iniţiată la eliberarea
fiecărei zone ocupate. Această metodă de compactare consumă prea mult timp (fiind prea des
invocată).

175
C. Alocare segmentată şi paginată

Ideea alocării segmentate şi paginate este aceea că alocarea spaţiului pentru fiecare segment să se facă
paginat. Pentru aceasta, iniţial fiecare proces aflat în starea ÎNTRERUPT are propria lui tabelă de
segmente. Apoi, fiecare segment încărcat în memoria operativă are propria lui tabelă de pagini. Fiecare
intrare în tabela de segmente are un câmp care indică adresa de început a tabelei de pagini proprii
segmentului.

5. Gestiunea memorie externe. Sisteme de fişiere. Tehnici input-output

Colecţiile mari de date, dar şi informaţiile gestionate de sistemul de operare pentru realizarea funcţiilor
sale sunt stocate în memoria externă prin intermediul fişierelelor şi a cataloagelor (directoarelor,
mapelor) de fişiere.

5.1. Gestiunea fişierelor

În cadrul unui volum extern, datele sunt grupate de către sistemul de operare în ansambluri de date
numite fişiere. Utilizatorul, prin programele sale, are acces - la un moment dat - numai la o parte dintr-
un fişier numită articol sau înregistrare. În general, un articol al unui fişier reprezintă o subdiviziune
a fişierului (un caracter, o secvenţă de caractere) ce constă din subgrupuri de date numite câmpuri.
Fiecare câmp al unui articol are o anumită valoare în fişier. O pereche (câmp, valoare) se numeşte
cheie de articol. Este posibil ca într-un fişier să existe mai multe articole cu aceeaşi cheie, sau se pot
imagina chei care să nu existe în nici un articol. De asemenea, este posibil ca într-un fişier să existe
câmpuri cu valori care nu se repetă. Un index este un câmp cu proprietatea că pentru oricare două
articole diferite ale fişierului, valorile câmpului sunt diferite.

Fiecare câmp al unui articol are un domeniu (mulţimea valorilor), precum şi o lungime de
reprezentare. Suma lungimilor de reprezentare a câmpurilor, împreună cu lungimea altor zone
completate de sistemul de operare, determină lungimea de reprezentare a articolului. Dacă lungimea de
reprezentare este aceeaşi pentru toate articolele unui fişier atunci spunem că articolele sunt de format
fix. În caz contrar, înregistrările se numesc articole de format variabil şi trebuie să existe un câmp în
care să fie înregistrată fie lungimea efectivă a articolului, fie un marcaj de sfârşit de înregistrare.

Fişierele pot fi organizate în mai multe moduri. Cel mai simplu mod de organizare este cel liniar, un
fişier fiind o colecţie de caractere. Semnificaţia caracterelor nu este cunoscută sistemului de operare ci
este impusă de programele utilizatorilor. Unele fişiere pot fi privite ca o colecţie de înregistrări de
lungime fixă. Astfel, sistemul de operare poate citi un articol şi poate scrie (prin suprascriere sau
adăugare la sfârşitul fişierului) o înregistrare. Sistemele de operare mai vechi structurau fişierele în
articole de lungime fixă. Cel mai interesant mod de structurare a informaţiei este organizarea
arborescentă. În acest caz, un fişier constă dintr-un arbore de înregistrări, de lungimi nu neapărat
egale, fiecare articol conţinând, într-o poziţie fixă, un câmp special numit index. Arborele este sortat
pe baza acestui index, pentru a permite accesul rapid la înregistrări.

Cele mai importante tipuri de acces la articolele unui fişier sunt: accesul secvenţial şi accesul direct.
Accesul secvenţial la al n-lea articol dintr-un fişier presupune efectuarea în ordine a n-1 accese la
articolele cu numerele de ordine 1, 2, ..., n-1. Accesul direct presupune existenţa unui mecanism de

176
obţinere a articolului căutat fără parcurgerea secvenţială a tuturor articolelor care îl preced. Sunt
posibile două tipuri de acces direct:
 acces direct prin adresă (număr de ordine). În acest caz programul utilizatorului furnizează
numărul de ordine al articolului, iar sistemul de operare întoarce articolul solicitat.
 accesul direct prin conţinut. Programul utilizatorului furnizează o cheie (câmp, valoare), iar
sistemul de operare întoarce articolul care îndeplineşte condiţia specificată.

Orice fişier este identificat printr-un specificator. Nu există reguli general valabile de specificare a
unui fişier. În general, referirea la un fişier se face printr-un şir de caractere, diferite de spaţiu. Şirul de
caractere este structurat în mai multe zone, plasate de la stânga la dreapta astfel:
 Numele dispozitivului periferic (DP) suport al fişierului. Aceste nume sunt impuse de sistemul
de operare. Sistemul de operare UNIX face excepţie de la această structurare deoarece orice
periferic este privit în UNIX ca un fişier obişnuit.
 Calea ce desemnează subcatalogul din cadrul suportului extern (disc, memorie flash etc.) unde
se află fişierul căutat.
 Numele fişierului (un şir de caractere, dependente de sistemul de operare) dat de creatorul
fişierului.
 Extensia sau tipul fişierului (un şir de caractere, dependente de sistemul de operare) dat tot de
creator. Fiecare sistem de operare poate cere utilizarea unor extensii speciale
 Alte informaţii. De exemplu, versiunea fişierului este un câmp ce ar putea fi utilizat pentru a
diferenţia mai multe versiuni ale aceluiaşi fişier.

În plus, sistemul de operare poate asocia unui fişier şi alte caracteristici, printre care: modul de
protecţie şi utilizatorii care au acces la fişier; biţi indicatori ai statutului fişierului (fişier cu acces
numai în citire, fişier ascuns, fişier sistem, fişier arhivat etc); poziţia şi lungimea indexului; data
creerii, a ultimei modificări şi a ultimului acces; dimensiunea curentă şi dimensiunea maximă la care
poate ajunge. Aceste caracteristici suplimentare se numesc atribute.

Gestiunea fişierelor în cadrul unui sistem de operare este realizată prin intermediul unui ansamblu de
subprograme, numit sistemul de gestiune a fişierelor (SGF), care asigură legătura între utilizatori şi
dispozitivul periferic care realizează operaţiile asupra fişierelor.

Acţiunile SGF pot fi stratificate astfel: acţiuni la nivel de articol; acţiuni la nivel de fişier şi acţiuni la
nivel de volum. Unele sisteme de operare oferă şi operaţii la nivel de câmp.

Acţiuni la nivel de articol:


 Citire (Read) - Datele sunt citite din fişier începând cu poziţia curentă. Solicitantul unei
operaţii de citire trebuie să indice cantitatea de date necesară şi o zonă de memorie unde să
fie depuse.
 Scriere (Write) - Datele sunt înregistrate în fişier începând cu poziţia curentă. Dacă
poziţia curentă este la sfârşitul fişierului atunci datele sunt scrise în continuare. Dacă
poziţia curentă este situată înainte de sfârşitul fişierului, datele existente sunt pierdute
deoarece datele noi se scriu peste cele vechi. De asemenea, solicitantul unei operaţii de
scriere trebuie să specifice adresa unei zone de memorie de unde se vor lua datele, precum
şi lungimea zonei de scris.
 Adăugare la sfârşitul fişierului (Append) - Prin această operaţie articolele pot fi scrise
numai la sfârşitul fişierului.
 Poziţionare (Seek) - Într-un fişier cu acces direct, această operaţie permite specificarea
directă a unui anumit articol. După poziţionare, articolul poate fi citit, sau în această
poziţie se poate scrie un articol.
177
Acţiuni la nivel de fişier:
 Deschiderea unui fişier (Open) - Acţiunile SGF care au loc înaintea primului acces la un
fişier sunt realizate prin intermediul operaţiei de deschidere (open) a fişierului respectiv.
Prin operaţia Open, sistemul de operare este informat că un anumit fişier va deveni activ.
În general, se cere ca înaintea primei citiri sau scrieri, fişierul să fie deschis. Prin
deschidere se face legătura între informaţiile de identificare a fişierului şi variabila din
program prin intermediul căreia utilizatorul se referă la fişier. Operaţia de deschidere
poate fi executată atât asupra fişierelor care există deja pe disc cât şi a fişierelor noi. În
funcţie de condiţiile de deschidere, prin operaţia Open se parcurg următoarele etape:
o Pentru un fişier existent:
 se realizează legătura între variabila din program ce indică fişierul,
suportul pe care se află păstrat şi descriptorul de fişier aflat pe disc;
 se verifică, dacă utilizatorul are drept de acces la fişier.
o Pentru un fişier nou, care urmează a fi creat, este necesară alocarea spaţiului pe
disc pentru memorarea viitoarelor articole ale fişierului;
o Se alocă memorie internă pentru zonele tampon necesare accesului la fişier;
o Completează o structură de date numită blocul de control al fişierului (File
Control Bloc) cu informaţiile obţinute în etapele anterioare.
 Închiderea fişierelor (Close) - Prin această operaţie, fişierul încetează de a mai fi activ.
Se actualizează informaţiile generale despre fişier: lungime, data ultimului acces etc.
Pentru fişiere nou create se adaugă o nouă intrare în catalogul discului. Pentru toate
fişierele, se adaugă când este cazul, marcajul EOF după ultimul articol al fişierului. Se
golesc (dacă e cazul) zonele tampon. De asemenea, se eliberează spaţiul de memorie
ocupat de FCB şi zonele tampon.
 Creare (Create) - Fişierul este creat fără date. Scopul operaţiei constă în stabilirea unor
atribute ale fişierului. Uneori se realizează o anumită formatare în vederea încărcării
ulterioare a datelor.
 Ştergere (Delete) - În momentul în care un fişier nu mai este necesar, el poate fi şters.
Astfel, spaţiul ocupat pe disc se eliberează şi se elimină intrarea din catalogul care conţine
descriptorul fişierului.
 Fixare atribute - Prin această operaţie anumite atribute ale fişierului sunt stabilite la
anumite valori (de exemplu, comanda UNIX chmod pentru stabilirea atributelor de
protecţie).
 Aflare atribute - Uneori, într-o aplicaţie, utilizatorul are nevoie de atributele unui fişier.
De exemplu, în sistemul de operare UNIX, programul make (utilizat în elaborarea
aplicaţiilor software), are nevoie de atributele fişierelor cu care lucrează. Mai precis, make
examinează timpul ultimului acces pentru scriere în fişierele în cauză şi efectuează
anumite operaţii numai asupra fişierelor care au fost modificate de la ultima utilizare de
către make.
 Redenumire (Rename) - Această operaţie schimbă numele unui fişier existent.

Alte acţiuni:
 Copierea unui fişier în alt fişier (Copy) - Dacă este necesară o copie fizică a fişierului sursă
(emiţător) într-un fişier destinaţie (receptor) atunci se utilizează operaţia de copiere.
 Listarea unui fişier (type sau print) - Această operaţie este un caz particular de copiere.
Numai fişierele de tip text pot face obiectul acestei operaţii. Fişierul destinaţie este ecranul
terminalului (type) sau imprimanta (print).
 Concatenarea mai multor fişiere (cat sau copy+) - Operaţia este o generalizare a copierii. De
exemplu, în sistemul de operare UNIX comanda

178
$ cat f1 f2 f3 > f4
realizează concatenarea fişierelor f1, f2 şi f3 şi depune rezultatul în fişierul f4.
 Compararea a două fişiere: este realizată prin parcurgerea secvenţială a celor două fişiere.
 Sortarea articolelor unui fişier (sort) - Prin această operaţie articolele fişierului iniţial sunt
ordonate conform anumitor specificaţii, iar rezultatul operaţiei se depune într-un nou fişier.
 Operaţii de filtrare a fişierelor - Un filtru este un program care citeşte un fişier şi creează
altul pe baza unor specificaţii. De exemplu, fişierul rezultat se poate obţine prin: sortare,
eliminare de articole cu un anumit conţinut, prelucrarea unui fişier conform unor machete etc.
 Compresia datelor dintr-un fişier - Această operaţie se realizează asupra fişierelor de
dimensiune mare care urmează a fi arhivate. Există multe tehnici de compresie, iar pentru
fiecare program de compresie există un program care reface informaţia iniţială.

5.2. Gestiunea cataloagelor

Din punct de vedere logic, cataloagele pot fi organizate pe nivele (unul sau mai multe), arborescent
sau sub forma unui graf aciclic (un digraf fără cicluri).

Structura cu un singur nivel reprezintă cel mai simplu mod de organizare. Toate fişierele de pe disc
sunt înregistrate în acelaşi catalog. O astfel de structură impune o serie de restricţii asupra lungimii
numelui fişierelor şi a numărului maxim de fişiere. Numărul de intrări în catalog este limitat de
dimensiunea catalogului. Structura cu două niveluri presupune existenţa unui catalog principal (master
file directory) care conţine câte o intrare pentru fiecare utilizator al sistemului. Această intrare este un
pointer către un catalog utilizator. Intrările într-un catalog utilizator descriu fişierele utilizatorului
respectiv. Modul de organizare arborescentă extinde în mod natural structura cu două nivele.

Un fişier catalog se deosebeşte de un fişier obişnuit numai prin informaţia ce o conţine. Un catalog
conţine lista numelor şi adreselor fişierelor subordonate lui. În plus, fiecare catalog are două intrări cu
nume speciale şi anume: "." (punct) - se referă la însuşi catalogul respectiv şi ".."(punct-punct) care se
referă la catalogul părinte. Fiecare volum extern conţine un catalog principal numit catalog rădăcină
(ROOT). Prin intermediul unor programe specializate (comenzi), un utilizator poate: schimba
catalogul curent (cd); poziţiona unele drepturi de acces (chmod); crea subcataloage subordonate
catalogului curent (mkdir); şterge un subcatalog creat anterior (rmdir); afişa calea de acces de la
catalogul ROOT la cel curent (pwd) etc.

Utilizatorul poate specifica o cale de acces în două moduri: absolut şi relativ. Structura de graf aciclic
este utilă atunci când anumite fişiere sau cataloage trebuie să fie accesibile din mai multe cataloage
(fişierele respective sunt resurse partajate).

Orice sistem de operare orientat pe gestiunea cataloagelor pune la dispoziţie apeluri sistem pentru
lucrul cu cataloage. Cele mai importante operaţii asupra cataloagelor sunt: creare, ştergere, deschidere,
închidere, citire, redenumire, creare de legături şi eliminare de legături:
 Crearea unui catalog (CREATEDIR) înseamnă crearea unui fişier cu două intrări standard:
"." şi "..". Această operaţie este realizată folosind o comandă a sistemului de operare (mkdir
(UNIX şi MS-DOS) sau md (MS-DOS)).
 Ştergerea (DELETEDIR) unui catalog înseamnă ştergerea unei intrări din catalogul părinte.
Un catalog poate fi şters numai dacă în afară de intrările standard nu mai conţine şi alte intrări.
 Deschiderea unui catalog (OPENDIR) este necesară pentru realizarea operaţiei de citire a
fişierului catalog. De exemplu, un program special (DIR în MS-DOS, ls în UNIX etc.) va
deschide catalogul şi va citi numele fişierelor din catalog.
179
 Închiderea unui catalog (CLOSEDIR) este necesară dacă nu mai sunt operaţii de executat
asupra unui catalog descris anterior. Prin această operaţie, zona de memorie alocată la
deschiderea fişierului catalog va fi eliberată.
 Prin operaţia de citire (READDIR) se va obţine intrarea următoare din fişierul catalog supus
citirii.
 Cum cataloagele sunt similare fişierelor, acestea pot fi redenumite în acelaşi mod. Este
realizată astfel operaţia de redenumire a unui catalog (RENAMEDIR).
 Prin crearea unei legături (operaţia LINK), un fişier poate să apară în mai multe cataloage.
Pentru realizarea acestei operaţii se specifică atât fişierul cât şi calea ce indică catalogul care
va deveni părinte al fişierului. În acest catalog se va introduce o intrare specială pentru fişierul
în cauză. Prin acest mecanism, un fişier este vizibil în mai multe cataloage.
 Prin ştergerea unei legături (operaţia UNLINK) este înlăturată intrarea specială din catalog;
c|nd catalogul curent este chiar catalogul în care s-a creat fişierul şi acesta nu mai este legat la
nici un alt catalog, atunci fişierul va fi şters.

O altă operaţie constă în montarea şi demontarea unui sistem de fişiere. Operaţia de montare constă în
conectarea unui sistem de fişiere de pe un anumit disc, la un catalog vid existent în sistemul de fişiere
de pe discul implicit. Operaţia de demontare are efect invers.

5.3. Drivere de Intrare/Ieşire (I/E)

Un driver de I/E este o componentă sistem sau utilizator care asigură interfaţa dintre nucleu şi unul sau
mai multe DP. Fiecare driver este un program separat, scris în limbaj de asamblare, C etc., şi compilat
într-un fişier executabil. Un driver trebuie să îndeplinească următoarele funcţii: 1) recepţionează şi
prelucrează întreruperile generate de DP; 2) iniţiază operaţiile I/E solicitate driver-ului de către nucleu;
3) forţează terminarea unor operaţii I/E în curs de desfăşurare; 4) efectuează alte funcţii (specifice
fiecărui DP), în cazul recuperărilor din avarie sau expirării unui interval de timp.

Un driver I/E poate fi rezident sau încărcabil. Un driver rezident constituie o parte componentă a
nucleului sistemului de operare, incorporată la generarea (prin compilare) sistemului. Un driver
nerezident constituie o extensie a nucleului, ce poate fi încărcată sau eliminată dinamic din memorie.
În cazul în care un utilizator doreşte să conecteze la configuraţia sa hardware un DP pentru care nu
există un driver, este necesară scrierea acestuia de către utilizator.

Un driver poate fi de tip caracter sau de tip bloc. El nu poate asigura, în acelaşi timp, funcţii de tip
caracter şi funcţii de tip bloc. Considerat a fi o parte a nucleului, un driver posedă un context propriu,
poate inhiba şi valida sistemul de întreruperi, putând sincroniza şi accesul la structurile de date ale
sistemului de operare împreună cu alte componente ale nucleului. Un driver poate suporta mai multe
DP, fiecare cu mai multe unităţi, toate lucrând în paralel. În acest caz însă, este necesară şi o
sincronizare internă.

Pentru rezolvarea funcţiilor de mai sus, un driver poate avea mai multe puncte de intrare. De exemplu,
un driver poate avea 5 puncte de intrare: tratarea intreruperilor, iniţierea unei operaţii I/E, tratarea
expirării timpului, terminarea forţată şi recuperarea din avarie. Tratarea de întrerupere este lansată
atunci când un DP, activat de driver, termină o operaţie I/E generând o întrerupere către procesor.
Legătura DP-driver este directă, prin intermediul vectorilor de întrerupere. Iniţierea unei I/E este
activată de către nucleu atunci când există cereri I/E în coada de aşteptare a driver-ului, semnalizându-
i acestuia necesitatea preluării lor. Lansarea operaţiei I/E se face prin depunerea informaţiilor necesare
în registrele adaptorului (cuplorului) unui DP. Pentru tratarea expirării timpului se utilizează un

180
contor de numărare a timpului iniţializat la iniţierea unei operaţii I/E. Dacă funcţia nu se termină în
intervalul de timp specificat, nucleul SO apelează driver-ul pe această intrare pentru ca el să poată lua
o decizie. Terminarea forţată a unei I/E este activată de către nucleu atunci când, în anumite
împrejurări, e necesar ca driver-ul să termine o operaţie I/E lansată, dar necompletată. Recuperarea din
avarie este iniţiată de nucleu în trei cazuri distincte: la restabilirea tensiunii de alimentare; la
iniţializarea sistemului, respectiv la încărcarea în memorie a unui driver încărcabil. În aceste cazuri se
efectuează o reiniţializare funcţională a driver-ului pentru reluarea corectă a lucrului.

6. Protecţie şi securitate

Sistemele de operare de tip UNIX fiind sisteme multiutilizator şi time-sharing, impun existenţa unui
utilizator special numit administrator de sistem (root), care ţine evidenţa utilizatorilor, stabileşte
parolele şi drepturile de acces şi creează cataloagele asociate utilizatorilor. Pentru fiecare utilizator,
administratorul de sistem creează câte un catalog propriu, care poate conţine atât fişiere ordinare
(programe sau date), cât şi subcataloage. Sistemele UNIX fac deosebire între litere mari şi litere mici.
Toate fişierele sunt structurate în cataloage, organizate arborescent, în vârful ierarhiei (la rădăcina
arborelui) aflându-se catalogul rădăcină (root) notat prin ‘/’. La pornirea unui terminal, sistemul de
operare afişează mesajul/fereastra de conectare în vederea identificării utilizatorului. Utilizatorul va
introduce un nume (numai cu litere mici) urmat de ENTER. Apoi sistemul va solicita parola. Dacă
identificarea utilizatorului s-a executat cu succes, sistemul de operare afişează fereastra de lucru sau un
şir de caractere numit prompter. Se mai spune că utilizatorul a deschis o sesiune de lucru. Închiderea
sesiunii curente se poate realiza folosind secvenţa CTRL+D sau Logout/Deconectare.

După deschiderea sesiunii, utilizatorul are acces la două grupe de fişiere: fişierele create de el însuşi,
respectiv fişierele furnizate de sistem drept comenzi sistem. Comenzile sistemelor de tip UNIX sunt
foarte variate: comenzi de administrare, comenzi de dezvoltare de programe, comenzi operator,
comenzi pentru comunicaţie în reţea etc. Cele mai importante comenzi operator sunt:
 cd (schimbarea catalogului curent);
 pwd (afişarea catalogului curent);
 mkdir (crearea unui catalog);
 rm (ştergerea unui fişier);
 mv (redenumirea unui fişier);
 rmdir (eliminarea unui catalog);
 ls (listarea cuprinsului unui catalog) etc.

Pentru dezvoltarea de programe, sunt disponibile editoare de texte (vi, ed etc.), compilatoare (cc, cp
etc.), utilitare diverse.

Pentru asigurarea protecţiei fişierelor şi cataloagelor se utilizează mecanisme specifice care să


restricţioneze accesul utilizatorilor neautorizaţi. O primă măsură de protecţie a datelor o reprezintă
drepturile de acces la fişier. Cât un utilizator nu are drepturi de administrator pe un anumit sistem de
calcul, acel utilizator se supune drepturilor de acces la fişiere pentru operaţii precum: citire, scriere,
executare, modificare, respectiv ştergere.

Există două metode mai întâlnite pentru definirea drepturilor de acces la fişiere: drepturi de acces la
nivel de utilizator/grup, respectiv acces prin liste – Access Control Lists (ACL). Prima metodă
(drepturi de acces la nivel de utilizator/grup) constă în definirea unor drepturi pentru următoarele
entităţi: posesorul unui fişier (user); grupul care deţine fişierul (group); toţi ceilalţi utilizatori (others).
Un utilizator se poate afla în mai multe grupuri. Această metodă este cea mai folosită cale pentru
181
definirea drepturilor de acces în UNIX. Metoda oferă un nivel de protecţie suficient pentru majoritatea
situaţiilor. În cadrul sistemului cu liste de acces, unui fişier i se pot asocia mai mulţi utilizatori şi/sau
grupuri de utilizatori. Pentru fiecare dintre aceste entităţi pot fi configurate mai multe tipuri de
drepturi. Sistemele de fişiere de tip Unix care au implementate standard drepturile de acces la nivel de
user/grup au la bază următorul set de drepturi de acces: citire (read) – deschidere şi citire de fişiere,
scriere (write) –creare şi scriere de fişiere, respectiv executare (execute) – executare fişiere/intrare în
directoare.

Utilizatorul cu drepturile cele mai mari este „root”-ul. De regulă nu se foloseşte root ci utilizatorul
normal. Vizualizarea drepturile de acces pentru un anumit fişier se realizează prin utilizarea comenzii
ls cu parametrul -l (sau a comenzii ll). Prin chmod se permite modificarea drepturilor, iar prin chown
se permite modificarea proprietarului şi a grupului căruia îi aparţine respectivului fişier.

7. Interfeţe utilizator

Comunicarea dintre sistemul de operare şi utilizator se realizează prin intermediul meniurilor sau
pictogramelor (disponibile prin intermediul interfeţelor grafice), respectiv prin dialog text în cadrul
modului linie de comandă (fereastra terminal).

8. Exerciţii şi probleme

1. Ce este un sistem de operare?


a. ansamblul elementelor hardware incorporate în calculator
b. ansamblul elementelor software incorporate în calculator
c. un program sau un grup de programe care asigură exploatarea eficientă a resurselor
hardware şi software ale unui sistem de calcul
d. kernelul

2. Un sistem de operare se mai numeşte


a. software de bază
b. compilator
c. editor de text
d. kernel

3. Cea mai importantă tehnică de exploatare optimă a procesorului introdusă în sistemele cu procesare
pe loturi:
a. partajare a timpului
b. multiprogramare
c. programare paralelă
d. executare inlanţuită

4. Un sistem de operare care permite comunicarea permanentă între utilizator şi sistemul de calcul este
un sistem cu
a. multiprogramare
b. programare paralelă
c. pelucrare pe loturi
182
d. partajare a timpului

5. Care sunt cele mai importante obiecte gestionate de un sistem de operare?

6. Ce este un sistem cu multiprogramare?

7. Enumeraţi trei caracteristici principale ale unui sistem de operare

8. Care sunt stările proceselor?

9. Descrieţi principalele operaţii asupra proceselor

10. Descrieţi tranziţiile posibile ale unui proces pe timpul duratei sale de viaţă.

11. Particularităţi ale proceselor UNIX.

12. Apelul fork();

13. Care sunt condiţiile pe care trebuie să le satisfacă soluţia corectă a problemei excluderii mutuale?

14. Metoda variabilei poartă. Descriere. Caracteristici

15. Metoda alternării. Descriere. Caracteristici

16. Metoda lui Peterson. Descriere. Caracteristici

17. Metoda semafoarelor. Descriere. Caracteristici

18. Mecanisme de sincronizare în sisteme de tip UNIX

19. Funcţii UNIX care permit sincronizarea controlată de utilizator

20. Mecanismul producător-consumator

21. Criteriile pe care trebuie să le satisfacă un algoritm ideal de planificare

22. Strategia FIFO pentru planificarea proceselor

23. Strategia LIFO pentru planificarea proceselor

24. Strategia "cel mai scurt timp de executare"

25. Strategia "Round Robin"

26. Alocarea prin reciclare bazată pe mai multe fire de aşteptare

27. Metoda camarazilor. Descriere

28. Metoda numerelor lui Fibonacci

29. Metoda "Alocarea integrală şi permanentă"


183
30. Metoda "Utilizator unic şi rezident"

31. Utilizarea partiţiilor variabile

32. Metoda transferului minim

33. Metoda suprapunerii minime

34. Metoda reacoperirii întârziate

35. Alocarea paginate

36. Clase de algoritmi de înlocuire a paginilor

37. Alocare segmentată

38. SGF: Acţiuni la nivel de articol

39. SGF: Acţiuni la nivel de fişier

40. SGF: Acţiuni la nivel de catalog

41. Drepturi de acces

42. Protecţia fişierelor

43. Explicaţi diferenţa între aşteptare activă şi blocare.

44. Examinaţi algoritmul lui Peterson şi arătaţi cum funcţionează această schemă dacă ambele procese
apelează simultan procedura intrare_în_secţiune_critică.

45. Descrieţi modul de implementare a mecanismului semafoarelor folosind dezactivarea


întreruperilor.

46. O generalizare a problemei producătorului şi consumatorului o constituie problema cititorilor şi


scriitorilor. Se presupune că există două tipuri de procese: cititor şi scriitor. Ele utilizează în comun o
aceeaşi resursă, de exemplu un fişier. Un proces scriitor modifică conţinutul fişierului, iar unul cititor
consultă informaţiile din el. Orice proces scriitor are acces exclusiv la resursă, în timp ce mai multe
procese cititor pot avea acces simultan la ea. Să se descrie o soluţie de rezolvare a acestei probleme
folosind semafoare.

47. Cei mai mulţi algoritmi "round-robin" utilizează o cuantă de dimensiune fixă. Argumentaţi de ce ar
fi necesară o valoare mică a cuantei. Dar o valoare mare a cuantei?

48. Cinci procese A, B, C, D, E sunt create în acelaşi timp. Se estimează ca ele necesită execuţii
complete de 10, 6, 2, 4 şi 8 secunde. Priorităţile acestora sunt 3, 5, 2, 1 şi 4, valoarea 5 desemnând o
prioritate mare. Ignorând timpul necesar trecerii de la un proces la altul, arătaţi evoluţia proceselor
folosind algoritmul round-robin cu cuanta 1.

184
49. Cinci procese A, B, C, D, E sunt create în acelaşi timp. Se estimează ca ele necesită execuţii
complete de 10, 6, 2, 4 şi 8 secunde. Priorităţile acestora sunt 3, 5, 2, 1 şi 4, valoarea 5 desemnând o
prioritate mare. Ignorând timpul necesar trecerii de la un proces la altul, arătaţi evoluţia proceselor
folosind algoritmul de planificare pe bază de prioritate.

50. Cinci procese A, B, C, D, E sunt create în acelaşi timp. Se estimează ca ele necesită execuţii
complete de 10, 6, 2, 4 şi 8 secunde. Priorităţile acestora sunt 3, 5, 2, 1 şi 4, valoarea 5 desemnând o
prioritate mare. Ignorând timpul necesar trecerii de la un proces la altul, arătaţi evoluţia proceselor
folosind algoritmul de planificare conform timpului minim de execuţie.

51. Cinci procese într-un sistem cu multiprogramare au duratele de executare completă de 9, 6, 3, 5 şi


respectiv X secunde. Care este ordinea de executare a proceselor astfel încât timpul mediu de aşteptare
în sistem să fie minim. Discuţie în funcţie de valorile lui X.

52. Un sistem de calcul utilizează metoda camarazilor pentru gestiunea memoriei. Iniţial el dispune de
un bloc de 256K la adresa 0. Presupunem că au loc cereri succesive pentru: 5K, 25K, 35K şi 20K. Ce
blocuri au mai rămas, care este dimensiunea şi adresa acestora?

53. Care este diferenţa dintre o adresă fizică şi o adresă virtuală?

54. Care este diferenţa între segmentare şi paginare?

55. Sistemele interactive de tip multiuser au introdus:


a. modul de lucru numit time-sharing;
b. modul de lucru numit pipe-line;
c. interfeţele de tip linie de comandă sau grafica;
d. toate trei.

56. Modul de lucru numit time-sharing:


a. combină interactivitatea cu multiprogramarea;
b. a apărut odată cu sistemele interactive de tip multiutilizator;
c. atât (a) cât şi (b);
d. nici (a), nici (b).

57. Pentru conducerea directă, interactivă a unor aplicaţii precum procesele tehnologice şi rezervările
de locuri sunt utilizate sistemele:
a. în timp real;
b. distribuite;
c. paralele.

58. Sistemele de operare care gestionează o arhitectură multiprocesor se numesc sisteme:


a. în timp real;
b. distribuite;
c. paralele.

59. Nucleul minimal al sistemului de operare care conţine funcţiile de bază se numeşte:
a. microkernel;
b. microkernel sau micronucleu;
c. micronucleu.

60. Serviciile asigurate de sistemele de operare sunt servicii orientate:


185
a. spre utilizatori (facilitarea utilizării sistemelor de calcul);
b. spre asigurarea operării eficiente a sistemului de calcul;
c. spre utilizatori şi spre eficientizarea utilizării resurselor de calcul.

61. Serviciile oferite de sistemele de operare utilizatorilor sunt:


a. executarea programelor, operaţiile de intrare-ieşire, comunicarea aplicaţiilor între ele, cu
calculatorul şi utilizatorii, detectarea erorilor;
b. executarea programelor, comunicarea aplicaţiilor între ele, cu calculatorul şi utilizatorii,
detectarea erorilor
c. executarea programelor, operaţiile de intrare-ieşire, detectarea erorilor;
d. operaţiile de intrare-ieşire, comunicarea aplicaţiilor între ele, cu calculatorul şi utilizatorii,
detectarea erorilor.

62. Serviciile prin care sistemele de operare asigură utilizarea eficientă a resurselor de calcul sunt:
a. alocarea resurselor, contabilizarea resurselor;
b. protejarea sistemelor de calcul, securitatea sistemelor de calcul;
c. alocarea resurselor, contabilizarea resurselor, protejarea sistemelor de calcul, securitatea
sistemelor de calcul.

63. Caracteristicile sistemului de operare UNIX sunt:


a. time-sharing;
b. portabil;
c. universal;
d. toate trei.

64. Interfaţa dintre sistemul de operare UNIX şi utilizatori se realizează prin intermediul:
a. unei componente hardware dedicate;
b. unui program special, numit shell;
c. kernelului.

65. Utilitarele sunt programe care pot realiza anumite prelucrări de bază prin apelarea secvenţială a
rutinelor kernel şi constau în principal din:
a. compilatoare şi instrumente pentru dezvoltarea aplicaţiilor;
b. instrumente pentru administrarea sistemului şi evaluarea performanţelor;
c. comenzi pentru lucrul cu fişierele şi directoarele/cataloagele;
d. toate cele de mai sus.

66. Sistemul de fişiere UNIX se caracterizează prin:


a. structura ierarhică;
b. protecţia fişierelor;
c. tratarea unui periferic în acelaşi mod ca un fişier;
d. toate cele de mai sus.

67. În sistemul de operare UNIX întâlnim următoarele directoare/cataloage principale:


a. /bin, /dev, /lib, /usr;
b. /bin, /dev, /more, /usr;
c. /bin, /cat, /lib, /usr.

68. Principalele comenzi UNIX pentru lucrul cu fişiere sunt:


a. cat, copy, find, grep, more, mv, rm;
b. cat, cp, find, grep, more, mv;
186
c. toate cele de mai sus.

69. Principalele comenzi UNIX pentru lucrul cu directoare/cataloage sunt:


a. mkdir, rmdir, cd, pwd, copy ;
b. mkdir, cd, pwd, mcopy ;
c. mkdir, rmdir, cd, pasw, copy.

70. Principalele comenzi UNIX pentru controlul proceselor sunt:


a. kill, id;
c. id, chmod, kill;
b. kill, ps;

71. Comanda ls din sistemul de operare UNIX:


a. Afişează conţinutul unui fişier
b. Afişează conţinutul unui catalog/director
c. Afişează lista de fişiere şi directoare/cataloage şi spaţiul ocupat de acestea
d. Afişează lista de partitii şi spatiul liber şi cel ocupat

72. Comanda ps din sistemul de operare UNIX:


a. Afişează conţinutul unui fişier
b. Afişează conţinutul unui catalog/director
c. Afişează lista de procese active
d. Afişează lista de partiţii şi spaţiul liber

73. Comanda kill din sistemul de operare UNIX:


a. Şterge conţinutul unui fişier;
b. Şterge directorul/catalogul curent;
c. Încheie programul curent;
d. Opreşte procesul indicat.

74. MMU (memory management unit) este:


a. o unitate de stocare pentru memorie;
b. o componentă a sistemului de operare
c. un fişier sistem
d. un dispozitiv hardware

75. Sistemul de operare Minix are o arhitectură:


a. monolitică
b. modulară
c. microkernel
d. hibridă

76. Funcţia pentru crearea unui nou proces în UNIX este:


a. execl
b. exec
c. fork
d. CreateProcess

77. Funcţia pentru returnarea id-ului procesului părinte în UNIX este:


a. getppid()
b. getpid()
187
c. id()
d. ps()

78. Funcţia pentru returnarea id-ului procesului fiu în UNIX este:


a. getcpid()
b. getcid()
c. fork()
d. cid()

79. Funcţia pentru eliminarea unui obiect de tip fişier în UNIX este:
a. kill
b. unlink
c. remove
d. del

80. Adresele generate de MMU sunt adrese:


a. Reale
b. Virtuale
c. Logice
d. Liniare

81. Analizati textul de mai jos şi explicaţi ce elemente moşteneşte procesul fiu.

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#define MAX_C 200
#define DIM 100
int main(void){
pid_t pid;
int i;
char X[DIM];
fork();
pid = getpid();
for (i = 1; i <= DIM; i++) {
sprintf(X, "LINIA procesului cu PID %d, val = %d\n",
pid, i);
write(1, X, strlen(X));
}
return 0;
}

82. Explicaţi fiecare din apelurile referitoare la sincronizarea proceselor create de codul de mai jos:

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
int pID = getpid();
char progn[1024];
gets(progn);

188
int cid = fork();
if(cid == 0) {
execlp(progn, progn, 0);
printf("Lipseste programul %s\n", progn);
} else {
sleep (1);
waitpid(cid, 0, 0);
printf("Program %s terminat\n", progn);
}
return 0;
}

83. Explicaţi fiecare din apelurile referitoare la sincronizarea proceselor create de codul de mai jos:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int pID = getpid();
int cid = fork();
if(cid == 0){
! sleep (5);
printf ( "Stop proces generat\n" );
exit (0); printf ( "Eroare!");
} else {
printf ( "Orice mesaj este bun.\n" );
char mesaj[10]; gets (mesaj);
if ( !kill(cid, SIGKILL) ) {
printf("Terminare proces fiu.\n");
} }
return 0;
}

84. Explicaţi fiecare din apelurile referitoare la comunicarea pipe din codul de mai jos:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
/* Scrie n mesaj(e) in fisierul f, cu pauza de o secunda intre
scrieri */
void scriitor (const char* mesaj, int n, FILE* f){
for (; n > 0; --n) {
fprintf (f, "%s\n", mesaj);
fflush (f); sleep (1);
}
} /* sf scriitor */

void cititor (FILE* f){


char X[1024];
while
(!feof (f) && !ferror (f) && fgets (X, sizeof (X), f) != NULL)
fputs (X, stdout);
189
} /*sf cititor */
int main (){
int pd[2];
pid_t pid;
pipe (pd); /* Creare pipe. */
pid = fork (); /* Generare proces fiu. */
if (pid == (pid_t) 0) {/* cod fiu */
FILE* f;
close (pd[1]);
f = fdopen (pd[0], "r"); /* deschidere in citire */
cititor (f);
close (pd[0]);
}
else { /* cod parinte */
FILE* f;
close (pd[0]);
f = fdopen (pd[1], "w"); /* deschidere in scriere */
scriitor (“Examen Licenta.”,5,f);
close (pd[1]);
}
return 0;
}

85. Explicaţi efectul secvenţei de cod:

int k;
for (k = 0; k < 5; k++) fork();

86. Ce afişează programul:

int main( void){


int pid, n = 5;
pid=fork();
printf("Generat %d\n", pid);
if ( pid) n=2;
printf("n = %d\n",n );
return 0;
}

87. Pentru codul de mai jos se cere explicarea sarcinilor realizate precum şi descrierea apelurilor
specifice gestionării proceselor.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h> /* pt macro WIFEXITED si WEXITSTATUS */
int main (void){
pid_t pid; /* pentru fork() */
int stare; /* pentru wait() */
pid = fork ();/* se creeaza procesul fiu */
switch (pid) {
190
case 0: /* acesta este procesul fiu */
if (execlp ("ls", "ls", "-l", 0) < 0) {
perror ("exec");
exit (EXIT_FAILURE);
} * sf if */
exit (EXIT_SUCCESS);
break;
default: /* acesta este procesul parinte */
break;
}
if (wait (&stare) < 0) {
perror ("wait");
exit (EXIT_FAILURE);
}
if (WIFEXITED (stare) && WEXITSTATUS (stare) == 0) {
if (execlp ("df", "df", "-h", 0) < 0) {
perror ("exec");
exit (EXIT_FAILURE);
}
}
return 0;
}

88. Scrieţi un program C care utilizează trei procese P0, P1 şi P2 astfel: procesul părinte P0 citeşte
numere de la tastatură şi le trimite la două procese fii P1 şi P2, acestea calculează sumele şi le trimit
inapoi la părintele P0, iar P0 adună cele două sume parţiale şi afişează rezultatul final.

89 [8]. Se consideră fragmentul de cod de mai jos. Indicaţi liniile care se vor tipări pe ecran în ordinea
în care vor apărea, dacă apelul fork() se execută cu succes? Justificaţi răspunsul.

int main() {
int k;
for(k=0; k<2; k++) {
printf("%d: %d\n", getpid(), k);
if(fork() == 0) {printf("%d: %d\n", getpid(), k);
exit(0);}
}
for(k=0; k<2; k++) {wait(0);}
return 0;
}

90. Completaţi programul următor astfel încât procesul părinte să trimită, prin PIPE, variabila n
procesului fiu şi să primească înapoi valoarea ei dublată.

int main() {
int n=1; if(fork() == 0) { ...........}
printf("%d\n", n);
return 1;
}

191
Bibliografie

1. Albeanu, G., Sisteme de operare, Ed. Petrion, 1996.


2. Albeanu, G., Arhitectura sistemelor de calcul, Editura FRM, 2007.
3. Ionescu, T., Saru, D., Floroiu, J.-W., Sisteme de operare, Ed. Tehnică, 1997.
4. Silberschatz, A., Galvin, P.B., Gagne, G., Operating Systems Concepts, Wiley & Sons, 2013
(ed. 9).
5. Stuart, B., L., Principles of Operating Systems: Design & Applications, Thomson Learning,
2009.
6. Tanenbaum, A.S., Modern Operating Systems, Pearson Educational International, 2015(ed. 4).
7. ***, Averian A., Sisteme de operare. Biblioteca Virtual/a, Universitatea Spiru Haret.
8. ***, Manual de Informatică pentru licenţă iunie și septembrie 2016,
http://www.cs.ubbcluj.ro/wp-content/uploads/Manual_Informatica_2016_RO.pdf.
9. ***, Examen de licenţă 2014, Model de subiect: Informatică, http://www.cs.ubbcluj.ro/wp-
content/uploads/Model_Subiect_2014_Informatica_Inf.pdf.

192

S-ar putea să vă placă și