48
Syllabus I. Informatii generale II.1 Datele de identificare a cursului Datele de contact ale titularului de curs: Nume: Gheorghe Cosmin Silaghi Birou: Campus, etaj 4, sala 431 Telefon: 0722-817582 Fax: 0264-412570 Email: [email protected] Consultatii: saptamanal, joia de la 12-14 Datele de identificare curs si contact tutori: Numele cursului: Limbaje de programare Codul cursului: EBI0085 Anul, semestrul: anul III, semestrul 1 Tipul cursului: obligatoriu Pagina web a cursului: www.econ.ubbcluj.ro/~gsilaghi/limbaje2008 Tutori: Gheorghe Cosmin Silaghi, Mircea Moca Adrese de email: [email protected] , [email protected] Conditionari si cunostiinte prerechizite: Se recomanda cunostiinte de programare structurata in C. Aceste cunostinte sunt obtinute la disciplina “Algoritmi si Structuri de date”, anul II, semestrul 2. Aditional, studentii pot opta pentru cursul facultative de Introducere in Programare, anul II semestru 1. Descrierea cursului Cursul de Limbaje şi medii de programare îşi propune să familiarizeze studenţii cu noţiunile principale legate de limbaje de programare. Studentii vor deprinde principalele noţiuni necesare pentru învăţarea unui limbaj de programare. Aici includem considerente legate de structura şi componentele unui limbaj de programare, modul de obţinere a unui program executabil dintr-un program sursă, tehnici de programare utilizate în conceperea eficientă a programelor. Pentru exemplificarea acestor notiuni, cursul va parcurge paradigma obiectuală şi va concretiza notiunile teoretice ale paradigmei prin utilizarea limbajului C++. Alegerea paradigmei obiectuale

Silabus Limbaje si medii de programare

Embed Size (px)

Citation preview

Page 1: Silabus Limbaje si medii de programare

Syllabus

I. Informatii generale

II.1 Datele de identificare a cursului

Datele de contact ale titularului de curs:

Nume: Gheorghe Cosmin Silaghi

Birou: Campus, etaj 4, sala 431

Telefon: 0722-817582

Fax: 0264-412570

Email: [email protected]

Consultatii: saptamanal, joia de la 12-14

Datele de identificare curs si contact tutori:

Numele cursului: Limbaje de programare

Codul cursului: EBI0085

Anul, semestrul: anul III, semestrul 1

Tipul cursului: obligatoriu

Pagina web a cursului: www.econ.ubbcluj.ro/~gsilaghi/limbaje2008

Tutori: Gheorghe Cosmin Silaghi, Mircea Moca

Adrese de email: [email protected], [email protected]

Conditionari si cunostiinte prerechizite:

Se recomanda cunostiinte de programare structurata in C. Aceste cunostinte sunt obtinute la

disciplina “Algoritmi si Structuri de date”, anul II, semestrul 2. Aditional, studentii pot opta

pentru cursul facultative de Introducere in Programare, anul II semestru 1.

Descrierea cursului

Cursul de Limbaje şi medii de programare îşi propune să familiarizeze studenţii cu noţiunile

principale legate de limbaje de programare. Studentii vor deprinde principalele noţiuni necesare

pentru învăţarea unui limbaj de programare. Aici includem considerente legate de structura şi

componentele unui limbaj de programare, modul de obţinere a unui program executabil dintr-un

program sursă, tehnici de programare utilizate în conceperea eficientă a programelor.

Pentru exemplificarea acestor notiuni, cursul va parcurge paradigma obiectuală şi va concretiza

notiunile teoretice ale paradigmei prin utilizarea limbajului C++. Alegerea paradigmei obiectuale

Page 2: Silabus Limbaje si medii de programare

ca şi ţinta de exemplificare se datorează importanţei acestei paradigme în industria software,

tehnologia obiectuală reprezentând una din cerintele fundamentale din industrie.

Mediul de programare utilizat va fi Visual C 6.0.

Organizarea temelor in cadrul cursului

Temele cursului sunt organizare conform logicii de invatare a conceptelor de programare

obiectuala. Cursul de bazeaza pe logica bulgarelui de zapada. Astfel, la inceput se introduce

notiuni facile legate de aceasta disciplina. Pe parcurs, aceste notiuni se folosesc la descrierea si

invatarea altor notiuni mai complicate. Studentii sunt rugati sa consulte bibliografia aferenta

fiecarei teme, atat din suportul de curs obligatoriu (manualul) cat si din celelalte carti indicate.

Pentru fiecare tema, suportul de curs dezvolta conceptele teoretice, prezinta exemple si propune

probleme de rezolvat. Pentru o buna aprofundare, studentii trebuie sa parcurga la calculator

aceste exemple si sa realizeze problemele date ca si tema. Site-ul disciplinei contine aditional

slide-urile pentru fiecare tema. Slide-urile contin informatii sumare, fiind un bun ghid de

reamintire a conceptelor dezvoltate la fiecare capitol. Slide-urile nu sunt suficiente ca si mijloc de

invatare a acestei discipline.

Formatul si tipul activitatilor de curs

La intalnirile cu studentii, profesorul va prezenta continutul teoretic al disciplinei. Studentii sunt

rugati sa participle la aceste intalniri, pentru ca au posibilitatea sa inteleaga mai bine conceptele

disciplinei. Continutul este disponibil si in manual, studentii avnad posibilitatea sa-l insuseasca

individual. Studiul individual presupune exercitiu la calculator in limbajul de programare C++, cu

exemplele din manual sau cu alte exemple. Pentru o mai buna intelegere, studentii pot sa resolve

problemele propuse sau sa realizeze alte programe software, la liberal or alegere.

Pentru fiecare tema, studentii trebuie sa trimita problemele propuse rezolvate pentru a fi

punctuate de catre tutori.

Materiale bibliografice obligatorii

1. G.C. Silaghi, Mircea Moca „Limbaje de Programare. Metode Obiectuale. Ghid teoretic si

practic”. Editia 2-a, Ed. Risoprint, 2008

2. Bruce Eckel, „Thinking in C++”, ed. Prentice Hall, 2000, vol 1.

3. Bjarne Stroustrup, „The C++ Programming Language”, Addison-Wesley,

4. Bazil Pârv, Al. Vancea, „Fundamentele limbajelor de programare”, 1996

5. D.L. Şerbănaţi, „Limbaje de programare şi compilatoare”, Ed. Academiei, 1987

6. Malcom Bull, „Student’s guide to programming languages”, HB Newnes, 1992

Page 3: Silabus Limbaje si medii de programare

Materiale si instrumente necesare pentru curs

- Laborator echipat cu MS Visual C 6.0

- videoproiector

Calendarul cursului

Cursul este impartit in capitole, aferente celor 14 saptamani ale unui semestru:

1. Introducere în limbaje de programare

1.1 Definirea limbajelor de programare

1.2 Clasificarea limbajelor

1.3 Istoricul şi evoluţia limbajelor

2. Fundamentele limbajelor de programare

2.1 Limbaje abstracte

2.2 Compilatoare

3. Introducere în obiectualitate

3.1 Principii de bază

3.2 Analiza şio designul programelor orientate obiect

3.3 Noţiuni de bază în C++

4. Abstractizare şi încapsulare

4.1 Abstractizarea

4.2 Ascunderea implementării

5. Construirea obiectelor

5.1 Constructori şi destructori

5.2 Supraîncărcarea funcţiilor

6. Tipuri de variabile

6.1 Constante

6.2 Funcţii inline

6.3 Elemente statice

6.4 Spaţii de nume

7. Transmiterea obiectelor între funcţii

7.1 Referinţe

7.2 Constructorul de copiere

8. Supraîncărcarea operatorilor

8.1 Realizarea supraîncărcării operatorilor

8.2 Excepţii de la regula de supraîncărcare

8.3 Considerente de design

8.4 Conversie automată de tip

Page 4: Silabus Limbaje si medii de programare

9. Gestiunea dinamică a memoriei

9.1 Crearea dinamică a obiectelor

9.2 Mecanismul de gestiune dinamică a memoriei

10. Compoziţie şi moştenire

10.1 Reutilizarea codului

10.2 Definirea moştenirii

10.3 Introducerea diagramelor de clase

10.4 Considerente de design

11. Polimorfism

11.1 Definirea conceptului de polimorfism

11.2 Realizarea polimorfismului de tip în C++

11.3 Clase abstracte

11.4 Aspecte colaterale ale moştenirii

12. Colecţii

12.1 Genericitate. Realizarea genericităţii

12.2 Colecţii heterogene

12.3 Colecţii parametrizate

13. Tipuri abstracte de date

13.1 Definirea ADT-urilor

13.2 Specificarea ADT-urilor

14. Paradigme de programare colaterale

14.1 Programarea bazată pe evenimente

14.2 Programarea bazată pe componente

14.3 Programarea logică şi funcţională

Fiecare tema presupune parcurgerea unui material intre 15 si 30 de pagini si rezolvarea a 3

probleme practice. In saptamanile 5, 9 si 14 studentii trebuie sa trimita spre evaluare tutorilor

rezolvarile la problemele propuse din capitolele anterioare saptamanii respective.

Politica de evaluare si notare

Nota la aceasta disciplina este compusa din:

- 70% examen,

- 30% evaluarea temelor din timpul semestrului

Termenele pentru teme sunt saptamanile 5, 9, si 14. Pe fiecare tema se acorda o nota de la 1 la 10

si se face media acestor note. Aceasta medie conteaza 30% din nota finala.

Page 5: Silabus Limbaje si medii de programare

Examenul din sesiune are o pondere de 70% din nota finala si este compus din examen scris si

practic. Examenul scris se compune dintr-un test grila cu 30 de intrebari, 5 intrebari deschise si o

problema. Examenul practice presupune rezolvarea unei probleme la calculator. Problema care se

rezolva la calculator are mai multe subpuncte, in ordine crescanda de dificultate. Nota

examenului este medie aritmetica a notelor de la proba scrisa si proba practica.

Elemente de deontologie academica

Stratregii de studiu recomandate

Se recomanda studentilor să parcurgă cursul gradual pe parcursul semestrului. Astfel, in timpul

parcurgerii fiecărui capitol, studentii sunt rugaţi să ruleze exemplele practice din material şi să

pună accept pe intelegerea conceptelor şi a modului in care programele sunt realizate si executate.

Studentii sunt incurajaţi să comunice cu tutorii in cazul in care există nelămuriri legate de

materialul cursului. Temele şi intrebările recapitulative indicate la fiecare capitol sunt minimale

pentru intelegerea si aprofundarea materialului. Recomandăm studentilor rezolvarea tuturor

problemelelor indicate in curs.

II. Suportul de curs propriu-zis

Page 6: Silabus Limbaje si medii de programare

MODULUL 1. FUNDAMENTELE LIMBAJELOR DE PROGRAMARE

Scopul si obiectivele mobulului:

În acest capitol vom prezenta succint formalizarea exhaustivă a teoriei limbajelor de programare.

Materiale precum [Aho 1977], [Şerbănaţi 1987] conţin o tratare detaliată a acestui subiect. Teoria

limbajelor de programare este necesară pentru specificarea formală corectă a limbajelor şi

reprezintă un fundament pentru teoria compilării.

1.1. Limbaje abstracte

1.1.1. Gramatici

Fie o mulţime A nevidă, finită, numită alfabet. Elementele acestei mulţimi se numesc simboluri.

Un simbol din A este reprezentat într-un limbaj printr-o literă, cifră sau semn, uneori printr-un

şir finit de litere, cifre sau semne.

Notăm prin *A mulţimea aranjamentelor cu repetiţie ale simbolurilor din A . Astfel, un element

din *A este un şir finit de simboluri din A . Simbolurile se pot repeta în şir. Mulţimea *A

conţine şi şirul vid; vom nota acest şir cu λ . Numărul de simboluri dintr-un şir din *A se

numeşte lungimea şirului. Se observă că dacă avem două şiruri x şi y din *A atunci şirul

xyz = , obţinut prin alăturarea simbolurilor din x cu simbolurile din y , va fi de asemenea,

element al mulţimii *A . Operaţia prin care se obţine şirul z din şirurile x şi y se numeşte

concatenare. Mulţimea *A înzestrată cu operaţia de concatenare are o structură de monoid.

Prin definiţie, un limbaj formal peste alfabetul A este o submulţime L a lui *A .

Fiind dat un alfabet A şi mulţimea *)(AP a părţilor mulţimii *A , pe această din urmă mulţime

putem defini următoarele operaţii de bază:

a. intersecţie: 21 LL ∩

b. reuniune: 21 LL ∪

c. complementare: }|*{ LxAxL ∉∈=

d. produs: },|{ 22112121 LxLxxxLL ∈∈= . Notăm cu ,...2 LLL =

e. stea: ......}{* 2 ∪∪∪∪∪= nLLLL λ

f. reflectare: }|~{~

LxxL ∈= unde x~ este imaginea reflectată a lui x

Page 7: Silabus Limbaje si medii de programare

În legătură cu un limbaj formal, se pune problema apartenenţei unei construcţii la limbaj, adică,

în condiţiile furnizării unui cuvânt *Ax∈ , dacă se poate decide (demonstra) una din următoarele

2 concluzii: Lx∈ sau Lx∉ .

Se spune că un limbaj L este decidabil, dacă răspunsul la întrebarea de mai sus este pozitiv.

Conceptul de algoritm stă la baza rezolvării acestei probleme. Adică, trebuie să decidem

răspunsul la întrebarea de apartenenţă într-un timp finit folosind operaţii precis definite. Problema

apartenenţei unui cuvânt la limbaj, în cazul limbajelor de programare trebuie să fie rezolvată de

compilator, adică acesta trebuie să decidă dacă codul sursă furnizat de programator satisface sau

nu regulile limbajului, adică poate fi compilat sau nu.

În cazul în care limbajul L este finit, atunci limbajul este decidabil. Dacă L este infinit, trebuie

să folosim alte metode pentru a răspunde la întrebarea de decidabilitate.

Specificarea limbajului înseamnă fie enumerarea tuturor elementelor acestuia, fie enunţarea unor

reguli de construcţie a elementelor limbajului. Noţiunea de gramatică stă la baza specificării unui

limbaj prin generarea tuturor cuvintelor sale.

Prin definiţie, un sistem formal este un cvadruplu ordonat >ℵ=< RFAS ,,, alcătuit din:

- alfabetul sistemului A

- mulţimea decidabilă a formulelor corecte, *AF ⊆

- mulţimea decidabilă a axiomelor, F⊆ℵ

- mulţimea finită a regulilor de deducţie (inferenţă) R . O regulă de deducţie de aritate 1+n

este o relaţie din mulţimea FF n × , care asociază o formulă unică x cu un n-tuplu

>=< nyyyy ,...,, 21 . Spunem că x se deduce din nyyy ,...,, 21 şi scriem Rxy .

Fie formulele corecte nyyy ,...,, 21 , numite premise. Fie ℵ∪= },...,,{ 210 nyyyE . Atunci, nE0

reprezintă mulţimea tuturor n-tuplelor cu formule din 0E . Atunci, aplicând succesiv regulile de

deducţie din R putem obţine mulţimile U1

11 },|{ incatastfel

≥−− ∈∃∪=

n

niii RxyEyxEE .

Dacă mulţimea premiselor e vidă, adică ℵ=0E atunci elementele lui iE se numesc teoreme.

Dacă x este o teoremă, atunci ea s-a obţinut prin aplicarea succesivă a unor reguli de deducţie

asupra unor formule din mulţimile iE . Secvenţa acestor reguli de deducţie alcătuieşte

demonstraţia teoremei x .

Prin definiţie, următorul caz particular de sistem formal:

a. alfabetul A al sistemului formal este finit şi este alcătuit din două mulţimi disjuncte N şi ∑ ,

alfabetul de simboluri neterminale respective alfabetul de simboluri terminale.

b. *AF = : toate şirurile finite pe alfabet sunt formule corecte

Page 8: Silabus Limbaje si medii de programare

c. ℵ conţine un singur element, şi anume un simbol neterminal S . Acest simbol se numeşte

simbol de început.

d. Regulile de deducţii au la bază producţii: O producţie este o pereche ),( βα de formule,

notată cu βα → . Regula de producţie asociată unei producţii este o regulă de rescriere, adică

QPQP βα → unde P şi Q sunt 2 şiruri din F . Aceasta înseamnă că în orice formulă

corectă care conţine subşirul α se poate înlocui α cu β şi se obţine tot o formulă corectă.

Notăm cu P mulţimea finită de producţii.

În aceste condiţii, sistemul formal >∑=< SPNG ,,, se numeşte gramatică.

Procesul de inferenţă în cazul gramaticilor se numeşte derivare.

Spunem despre două şiruri δ şi ε din F că δ derivează imediat în ε în cadrul unei gramatici

date, dacă există două şiruri 1γ şi 2γ din F şi o producţie βα → astfel încât 21αγγδ = şi

21βγγε = . O asemenea relaţie de derivare reprezintă o derivare într-un singur pas. Derivarea se

poate defini pe k paşi, în cazul ε este obţinut din δ prin k derivări.

O relaţie de derivare a lui ε din δ este nebanală, dacă obţinerea lui ε se realizează într-un număr

nenul de paşi. Relaţia de derivare se numeşte generală dacă ε se obţine din δ într-un număr

nenul de paşi sau dacă cele 2 formule coincid.

Prin definiţie, un limbaj generat de o gramatică G este mulţimea tuturor propoziţiilor }{)( xGL =

cu proprietatea că S derivă general pe x .

Dacă limbajele generate de două gramatici coincid, se spune că gramaticile sunt echivalente.

Conform definiţiei gramaticilor enunţată de mai sus, Naom Chomsky le-a clasificat după forma

producţiilor. Astfel, avem următoarele tipuri de gramatici:

1. Clasa gramaticilor de tip 0: reprezintă cea mai generală clasă de gramatici, sunt cele care

respectă definiţia generală furnizată mai sus

2. Clasa gramaticilor de tip 1: producţiile sunt de forma: αγββα →A . A este un simbol

neterminal, γ este şir oarecare, diferit de simbolul vid. Aceste gramatici se mai numesc şi

dependente de context.

3. Clasa gramaticilor de tip 2: producţiile sunt de forma: α→A , unde A este un simbol

neterminal, α este şir oarecare. Aceste gramatici se mai numesc şi independente de context.

4. Clasa gramaticilor de tip 3 sau regulate: producţiile sunt de forma aBA→ sau aA → , unde

A şi B sunt simboluri neterminale iar a este simbol terminal.

Corespunzător claselor de gramatici, avem clase de limbaje. Astfel, se poate defini o ierarhie

Chomsky a limbajelor.

Limbajul independent de context (de tip 2) este modelul limbajelor de programare.

Page 9: Silabus Limbaje si medii de programare

În ceea ce priveşte utilitatea studiului gramaticilor pentru scrierea compilatoarelor următoarea

teoremă este importantă:

Orice limbaj dependent de context (şi în consecinţă independent de context şi regulat) este

decidabil. Deci, limbajele de programare sunt decidabile.

1.1.2. Specificarea limbajelor de programare

1.1.2.1. BNF

Pentru descrierea limbajelor de programare se folosesc meta-limbaje. Acestea furnizează reguli

prin care se pot specifica şirurile acceptate de un limbaj de programare.

BNF (Backus Naur Form) reprezintă cel mai utilizat limbaj de specificare a limbajelor de

programare. În fond, BND descrie o gramatică independentă de context. În BNF, simbolurile

neterminare se scriu între paranteze unghiulare şi se definesc recursiv, prin meta-formule. De

asemenea, semnul ::= face parte din limbajul de specificare. El se poate traduce prin: „se

defineşte astfel”. Simbolul | înseamnă alegere, în sensul că permite utilizarea uneia din cele 2

alternative alăturate simbolului. Astfel, în BNF, simbolurile <, >, ::=, | se numesc meta-

simboluri.

Pentru a exemplifica utilizarea BNF, vom descrie în acest limbaj de specificare sintaxa de

compunere a unei propoziţii simple într-un limbaj informal. Astfel, avem următoarea definiţie

BNF:

<sentence>::=<subject><verb><object>.

<subject>::=<article><noun>|<subject pronoun>

<verb>::=sees|hits

<object>::=<article><noun>|<object pronoun>

<article>::=a|the

<subject pronoun>::=he|she

<object pronoun>::=him|her

În consecinţă, se constată faptul că o propoziţie este compusă prin alăturarea unui subiect, verb şi

a unui obiect (atribut). Propoziţia se termină cu simbolul terminal punct. Subiectul poate fi un

articol alăturat unui substantiv sau (simbolul |) un pronume de tip subiect. Pronumele de tip

subiect poate fi unul din simbolurile terminale he sau she etc.

Astfel, prin asemenea construcţii se pot descrie toate regulile de producţie din limbaj. De fapt,

prin BNF descriem întreaga gramatică pe baza căreia apoi, putem genera limbajul corespunzător

acesteia. Deci, pornind de la specificarea BNF a unei gramatici, putem folosi această specificare

pentru a genera limbajul şi pentru a recunoaşte sintagmele acceptate de gramatica definită.

Page 10: Silabus Limbaje si medii de programare

Procesul prin care, având dată la intrare o propoziţie, determinăm (decidem) dacă propoziţia

respectivă este acceptabilă, în contextul unei gramatici se numeşte parsare.

1.1.2.2. EBNF

De multe ori, specificarea BNF este greoaie din punct de vedere al lizibilităţii, mai ales atunci

când un simbol neterminal se poate repeta, de un număr finit sau infinit de ori, într-o construcţie.

De exemplu, putem considera definirea unui string ca fiind un şir de una sau mai multe cifre:

<string>::=<digit>|<digit><string>

Pentru a uşura asemenea construcţii, EBNF introduce următoarele meta-simboluri:

{} tot ce e inclus între acolade se poate repeta, sau poate lipsi

[] tot ce e inclus între parantezele drepte poate lipsi (este opţional)

Astfel, construcţia de mai sus poate fi scrisă:

<string>::=<digit>{<digit>}

De exemplu, pentru a defini un număr, care poate avea simbolul de semn, putem folosi

următoarele construcţii alternative:

<numer>::=<sign><unsigned>|<unsigned> în BNF

sau

<number>=[<sign>]<unsigned> în EBNF

EBNF introduce şi alte meta-simboluri ajutătoare. Astfel avem:

* înlocuieşte acoladele, are semnificaţia că simbolul precedent se poate repeta de un număr

de 0 sau mai multe ori

+ simbolul precedent se poate repeta de 1 sau mai multe ori

_ se subliniază meta-simbolurile, atunci când acestea fac parte din alfabetul limbajului

specificat

1.1.2.3. Diagrame de sintaxă

Reprezintă o descriere alternativă vizuală, a specificării unui limbaj. Au fost introduse la

specificarea limbajului Pascal. Diagramele de sintaxă reprezintă grafe orientate având ca noduri

simbolurile din limbaj iar săgeţile indică succesiunea acceptată a acestora.

Page 11: Silabus Limbaje si medii de programare

Să considerăm o gramatică specificată în limbaj EBNF.

<sentence>::=<expression>=

<expression>::=<number>[<operator><expression>]

<number>::=[<sign>]<unsigned>

<operator>::=*|+|-|/

<sign>::=+|-

<unsigned>::=<string>[.<string>]|.<string>

<string>::=<digit>{<digit>}

<digit>::=0|1|2|3|4|5|6|7|8|9

Figura 5 prezintă câteva diagrame de sintaxă pentru elementele gramaticii de mai sus.

<sentence> <expression> =

<digit> 0

1

2

3

4

5

6

7

8

9

<digit><string>

Figura 18.1 Diagrame de sintaxă

1.1.3. Automate de acceptare

Un automat de acceptare este folosit pentru a răspunde la întrebarea: un şir x aparţine limbajului

L sau nu?

Automatul este definit ca o maşină cu operaţii simple, care primeşte şirul de analizat pe un suport

oarecare, îl parcurge, şi răspunsul final este dat de starea în care rămâne unitatea de comandă a

automatului. Suportul pe care este furnizat şirul este denumit generic bandă de intrare, iar

variabila care parcurge şirul de analizat în citire se numeşte cap de citire. Automatul poate folosi

o memorie auxiliară pentru păstrarea unor informaţii care să fie de folos la un moment dat, în

procesul decizional.

Asemenea automate sunt definite prin grafe. Nodurile grafului reprezintă stări ale automatului, iar

arcele reprezintă tranziţii între stări. Arcele sunt marcate cu condiţii, cu semnificaţia că, dacă

Page 12: Silabus Limbaje si medii de programare

automatul se află într-o anumită stare, şi se îndeplineşte condiţia de pe un arc care iese din starea

respectivă, atunci automatul va trece în starea de la celălalt capăt al arcului selectat.

Automatele pot fi:

- deterministe, dacă dintr-o stare se poate realiza cel mult o singură mişcare

- nedeterministe, dacă dintr-o stare există mai multe mişcări posibile. Automatele

nedeterministe corespund gramaticilor cu producţii cu alternative.

Un şir x este acceptat de un automat U dacă, pornind de la configuraţia iniţială, prin mişcările

automatului se parcurge întregul şir de intrare şi automatul ajunge într-o configuraţie finală.

Pentru exemplificare, vom considera automatul finit din figura 6.

q0 q1a

b

ba

Figura 18.2 Diagrama de tranziţii a unui automat finit cu 2 stări

Astfel, automatul din figura 18.2 are 2 stări: q0 şi q1. q0 reprezintă starea iniţială, q1 reprezintă

starea finală. Dacă, pe banda de intrare se întâlneşte şirul „a” şi automatul este în starea q0, atunci

automatul trece în starea q1. Dacă pe banda de intrare se întâlneşte şirul „b” şi automatul este în

starea q0, atunci automatul rămâne în aceeaşi stare etc. Tabelul 2 prezintă matricea de definire

corespunzătoare acestui automat.

a b

q0 q1 q0

q1 q1 q0

Tabelul 18.1 Matricea automatului din figura 6

Pentru acest automat, şirul „aaba” este un şir acceptat deoarece:

(q0,aaba) a (q1,aba) a (q1,ba) a (q0,a) a (q1,λ )

Deci, pornindu-se din starea iniţială q0, se parcurge şirul de intrare şi în final, la epuizarea

acestuia, se ajunge în starea finală q1, cu şirul vid λ .

Exemplul din figura 18.1 reprezintă un automat finit determinist. În figura 18.2 prezentăm un

exemplu de automat finit nedeterminist. Tabelul 3 reprezintă matricea de tranziţii pentru acest

automat. Caracteristic automatului nedeterminist este faptul că dintr-un nod pot ieşi mai multe

săgeţi etichetate cu acelaşi simbol de intrare, precum şi săgeţi etichetate cu λ care reprezintă

tranziţii independente de intrare.

Page 13: Silabus Limbaje si medii de programare

0 1

2

3a b

b b

λ

a

a

Figura 18.2 Diagrama de tranziţie a unui automat finit nedeterminist

a b λ

0 {0,1} - {2}

1 - {2,3} -

2 {2} {3} -

3 - - -

Tabelul 18.2 Matricea automatului finit din figura 7

Se poate stabili o relaţie între automatele finite deterministe şi cele nedeterministe. Astfel, pentru

orice automat finit nedeterminist există un automat finit determinist care acceptă acelaşi limbaj.

Transformarea unui automat finit nedeterminist într-un automat finit determinist este importantă

deoarece lucrul cu automatele nedeterministe este dificil, datorită traiectoriilor paralele pe care le

poate lua calculul în aceste automate. Dar noi trebuie să privim aceste automate în contextul

studiului gramaticilor, care definesc limbajele de programare. Astfel, pornind de la o gramatică,

se poate genera un automat finit care să accepte gramatica respectivă. Dacă automatul finit

obţinut pentru o gramatică este nedeterminist, vom aplica procedura pentru construirea

automatului finit determinist echivalent.

Automatele deterministe ne indică modul în care trebuie să tratăm un şir de intrare pentru a

identifica dacă acesta este acceptat sau nu de automat. Astfel, la construirea compilatoarelor,

analizorul sintactic foloseşte logica automatului pentru a spune dacă o construcţie de intrare e

validă sau nu, şi în caz afirmativ, consideră mai departe această construcţie.

În practică, ori de câte ori avem de implementat un parser, adică un program care interpretează la

intrare şiruri de caractere cu anumite proprietăţi, prin descrierea automatului corespunzător putem

identifica traiectoriile posibile de intrare şi implementa un analizor corect şi eficient. În ANSI C

există biblioteca de expresii regulate folosite pentru generare de şabloane de şiruri de caractere,

care urmează exact regulile semantice de descriere ale automatelor.

Page 14: Silabus Limbaje si medii de programare

1.2. Compilatoare

În procesul de comunicare om-calculator intervine un program intermediar, translatorul, care

asigură traducerea programelor scrise de utilizator din cod sursă într-un alt limbaj mai apropiat de

calculator. Dacă limbajul ţintă este codul maşină, translatorul se numeşte compilator. Astfel,

execuţia unui program sursă se realizează, în cadrul limbajelor compilative, în 2 faze:

- compilare, care traduce codul sursă în program obiect

- execuţie, care rulează codul obiect pe calculator, folosind datele iniţiale ale programului şi

produce rezultate

Compilatorul unui limbaj de asamblare se numeşte asamblor.

În practică, pe lângă compilatoarele obişnuite, există şi alte tipuri de compilatoare.

Astfel, preprocesoarele sunt translatoare care traduc dintr-un limbaj de nivel înalt în alt limbaj de

nivel înalt. Preprocesorul limbajului C++ reprezintă un bun exemplu.

Cross-compilatoarele sunt compilatoare scrise pentru un calculator gazdă, în vederea generării de

cod pentru alt calculator. Cross-compilatoarele sunt folosite la scrierea de cod pentru diverse

dispozitive inteligente, care conţin procesoare.

În cazul limbajelor interpretative, compilatorul este de tip special, adică incremental. Astfel,

programul sursă este spart de către compilator în porţiuni mici numite incremente, care au o

oarecare independenţă sintactică şi semantică. Incrementele sunt traduse de compilator. Pe

măsură ce compilatorul traduce un increment, calculatorul execută incrementul tradus.

În mod tradiţional, un compilator realizează un şir de transformări asupra codului sursă în

reprezentări din ce în ce mai apropiate de codul maşină. Figura 10 prezintă fazele unui

compilator.

Analizalexicala

Analizasintactica

Analizasemantica

Optimizarede cod

Generare decod

Tratareaerorilor

Gestiuneatabelelor

Programsursa

Sir de atomilexicali

Arboresintactic

Codintermediar

Cod intermediaroptimizat

Programobiect

Figura 18.3 Fazele unui compilator [Şerbănaţi 1987]

Analiza lexicală grupează caracterele din program în subşiruri numite atomi lexicali care

reprezintă cuvintele cheie, operatori, constante, identificatori şi delimitatori.

Şirul de atomi lexicali este preluat de analiza sintactică. Aceasta depistează structuri sintactice

cum ar fi expresii, liste, instrucţiuni, proceduri. Aceste structuri sunt plasate într-un arbore

sintactic conform relaţiilor existente între aceste structuri.

Page 15: Silabus Limbaje si medii de programare

Analiza semantică foloseşte structura programului pentru extragerea informaţiilor privind

obiectele purtătoare de date (variabile, proceduri, funcţii), verificarea consistenţei utilizării lor. Pe

măsura parcurgerii arborelui sintactic, analiza semantică construieşte o reprezentare a codului

sursă în cod intermediar. Acesta este de obicei un şir de instrucţiuni simple cu format fix. Ordinea

operaţiilor din codul intermediar respectă ordinea de execuţie a acestora pe calculator.

Codul intermediar este prelucrat în faza de optimizare pentru eliminarea redundanţelor de calcule,

a calculelor şi variabilelor inutile, pentru o execuţie mai eficientă.

Generarea de cod alocă celule de memorie pentru memorarea datelor la execuţie. Se alocă

registre şi se produce cod obiect echivalent cu programul în limbaj intermediar.

Gestiunea tabelelor este de fapt, o colecţie de proceduri care creează şi actualizează datele cu care

lucrează celelalte faze. În această tabelă, pe lângă informaţii proprii compilatorului se găsesc şi

tabele ale identificatorilor, constantelor, cuvintelor cheie. Uneori avem o tabelă unică, numită

tabela simbolurilor.

Tratarea erorilor este o colecţie de proceduri care sunt activate ori de câte ori se depistează o

greşeală în program. De obicei, utilizatorul primeşte un mesaj de diagnostic. Dacă greşeala este

identificată în faza de analiză sintactică, compilatorul poate să-şi urmeze analiza pentru a detecta

şi alte erori.

Structura prezentată este mai mult conceptuală. Compilatoarele concrete de multe ori prezintă

abateri faţă de această structură. Unele componente pot lipsi, sau funcţionalitatea lor poate fi

preluată de alte componente, sau ordinea activării componentelor poate fi diferită.

Realizarea compilatoarelor presupune un volum mare de muncă. Există unelte software

specializate care asigură facilităţi de dezvoltare a compilatoarelor. Astfel, avem uneltele LEX şi

YACC sub Unix care permit descrierea sintactică şi semantică a unui compilator, conform

regulilor limbajului, utilizând automatele de acceptare a construcţiilor de intrare. Programatorul

trebuie să descrie regulile limbajului şi sintaxa propoziţiilor acceptate în format de expresii

regulate, iar mai apoi construcţia ţintă asociată fiecărei sintagme de limbaj sursă. Utilitarele

generează un cod C care apoi compilat, reprezintă de fapt compilatorul pentru limbajul sursă

considerat.

Datele de intrare pentru asemenea unelte sunt:

- specificaţia limbajului sursă, în ceea ce priveşte descrierea lexicului şi a sintaxei

- specificaţia limbajului ţintă şi a regulilor semantice de traducere

- specificaţia maşinii ţintă

Dacă în trecut, timpul de elaborare a unui compilator era destul de mare, azi, utilizând asemenea

tool-uri moderne putem realiza rapid compilatoare pentru diverse limbaje.

Page 16: Silabus Limbaje si medii de programare

1.3. Întreb ări recapitulative

1. Descrieţi clasificarea gramaticilor după Naom Chomsky.

2. De ce limbajul independent de context reprezintă modelul unui limbaj de programare?

3. Cum se specifică un automat? Care sunt tipurile de automate? Explicaţi importanţa studierii

automatelor cu privire la studiul limbajelor de programare.

4. Explicaţi structura (fazele) unui compilator. Ce se întâmplă în fiecare fază?

5. Se dă următoare specificare EBNF pentru un calculator de buzunar: (a) <expression>::=<number>{<operator><expression>}

(b) <sentence>::=<number>{<operator><sentence>}=

(c) <number>::={<sin>}<unsigned>

Sunt aceste reguli de specificare corecte?

6. Fie următoarele producţii: <unsigned>::=<string>[.<string>]

<string>::=<digit>{<digit>}

<digit>::=0|1|2|3|4|5|6|7|8|9

Se decidă care din următoarele numere sunt acceptate sau nu de gramatica specificată mai sus:

(a) 1

(b) 1.

(c) 1.1

(d) .1

(e) 12.34

7. Să se scrie automatul finit determinist care acceptă următorul limbaj: {a, b, ab, abab, …}

8. Fie gramatica regulată G=<{B, S}, {a, b}, P, S> unde

},,,,,{ bBaBbBBaBBaBSSP →→→→→→= λ . Descrieţi limbajul aferent acestei

gramatici în BNF. Trasaţi automatul finit care acceptă această limbajul asociat gramaticii.

Aplicaţi procedura de conversie a automatului nedeterminist în automat finit determinist şi

listaţi automatul determinist echivalent.

Page 17: Silabus Limbaje si medii de programare

MODULUL 2. INTRODUCERE IN PROGRAMAREA OBIECTUAL Ă

Scopul si obiectivele modulului

In acest modul urmează să prezentăm descrierea, realizarea si implementarea conceptelor

de programare obiectuală in C++. Aceste concepte sunt detaliate in curs [Silaghi 2008].

2.1. Paradigma programării obiectuale

Toate limbajele de programare realizează abstractizări. Atunci când scrie un program,

programatorul trebuie să gândească în limbajul înţeles de calculator pentru a rezolva o

problemă. Se stabileşte astfel o corespondenţă între modelul maşinii (în spaţiul soluţiilor)

pe care se rulează programul şi modelul problemei care se rezolvă prin intermediul

calculatorului.

Activitatea de programare presupune stabilirea acestei asociaţii pentru o problemă cerută

a se rezolva pe calculator. Pentru simplificarea acestei activităţi de programare, este

necesară inventarea unor limbaje de programare pentru care modelele maşinii (spaţiul

soluţiilor) să fie cât mai aproape de percepţia umană despre spaţiul problemei de rezolvat.

Programarea obiectuală asociază elementelor din spaţiul problemei obiecte în spaţiul

soluţiilor.

Principalele caracteristici ale unui limbaj de programare obiectual sunt:

1. Orice element este un obiect. Putem vedea obiectele ca şi variabile care memorează

date, dar în plus, putem adresa cereri obiectelor, solicitându-le să-şi schimbe starea.

2. Un program este o colecţie de obiecte. Obiectele sunt legate unele de altele,

transmiţându-şi mesaje. Putem vedea un mesaj ca şi un apel de funcţie.

3. Fiecare obiect are propriul spaţiu de memorie, şi este constituit din alte obiecte.

Astfel, se pot crea noi tipuri de obiecte împachetând obiecte existente.

4. Fiecare obiect are un tip.

5. Toate obiectele dintr-un anumit tip pot primi acelaşi mesaj.

Nume tip Ligth

Interfaţa on()

off()

Figura 19.1 Exemplu simplu de clasă

O clasă descrie un set de obiecte care au caracteristici şi funcţionalitate identice. Clasa

este un tip de date, aşa cum sunt cunoscute tipurile de date în C. Cererile care pot fi

Page 18: Silabus Limbaje si medii de programare

adresate unui obiect definesc interfaţa obiectului. Funcţiile care compun interfaţa unei

clase se numesc metode. Figura 19.1 prezintă exemplul clasei Ligth.

Astfel, dacă lt este un obiect de tipul Ligth, atunci putem cere acestui obiect realizarea

mesajului on():

Implementarea constă în modalitatea concretă prin care se realizează funcţionalitatea

descrisă de interfaţă. Astfel, în exemplul prezentat anterior, implementarea va consta în

acţiunile concrete care trebuiesc realizate (programate) pentru a realiza operaţia on().

În activitatea de scriere şi utilizare a programelor informatice, se disting 2 tipuri de actori:

- programatori de clase noi

- programatori care folosesc clasele create de creatorii de clase. Programele scrise de

aceştia se numesc programe client.

Scopul programatorilor de aplicatii client este de a colecta şi utiliza un mediu de lucru

care să conţină cat mai multe clase, pentru a asigura dezvoltarea rapidă aplicaţiilor.

Scopul programatorilor creatori de clase este de a construi clase care să furnizeze

programatorilor clienţi doar ceea ce este necesar, restul componentelor clasei urmând să

rămână ascunse.

Se ajunge astfel la conceptul de “ascundere a implementării ”, descris în C++ prin

cuvintele cheie public, private, protected. Aceste cuvinte cheie se numesc modificatori de

acces.

După ce o clasă a fost creată, testată şi începe să fie utilizată în programe client, se pot

construi noi clase care să conţină obiecte din clasa iniţială. Astfel, se realizează “re-

utilizarea implementării” , una din facilităţile principale oferite de programarea

obiectuală.

Procesul prin care se compune o nouă clasă înglobând de la clase existente se numeşte

compoziţie1 (agregare).

Dacă avem creată o clasă, şi dorim să creăm o clasă nouă, cu o funcţionalitate similară cu

a clasei iniţiale (o clasă care, eventual, să conţină aceeaşi funcţionalitate a clasei iniţiale,

extinsă cu proprietăţi noi), se foloseşte conceptul de moştenire. Clasa iniţială de la care

porneşte procesul de moştenire se numeşte clasă de bază.

În cazul în care prin moştenire, clasa nouă are exact aceeaşi interfaţă ca şi clasa de bază,

având doar o funcţionalitate diferită pentru metodele din interfaţă, spunem ca obiectele

din tipul clasei derivate sunt de tipul clasei de bază. Un asemenea tip de relaţie este

denumită generic relaţie de tip is-a.

1 Composition în lb. Engleză

Page 19: Silabus Limbaje si medii de programare

Dacă insă, clasa derivată adaugă funcţionalitate nouă prin crearea de noi metode în

interfaţă, atunci spunem că obiectele de tipul clasei derivate se aseamănă cu obiectele din

clasa de bază. Relaţia nou creată se numeşte generic relaţie de tipul is-like-a.

Prin moştenire, se pot crea ierarhii de clase. Între obiectele de tipul claselor din ierarhie

se pot stabili relaţii de tipul is-a sau is-like-a. Astfel, putem considera că obiectele din

clasele derivate sunt în acelaşi timp şi obiecte din tipul clasei de bază. Programarea

orientată obiect ne permite să utilizăm obiecte de tipul claselor derivate în locul

obiectelor de tipul clasei de bază. Se obţine astfel posibilitatea schimbării obiectelor din

clase diferite între ele, realizându-se polimorfism prin interschimbare de obiecte. Prin

polimorfism, compilatorul C++ va determina în momentul execuţiei tipul din care face

parte obiectul, apelând funcţionalitatea aferentă metodei solicitate.

Crearea şi distrugerea obiectelor reprezintă elemente importante de care trebuie să se

ţină seama la scrierea programelor. La crearea obiectelor, memoria care se alocă acestora

poate fi

- determinată la compilare. Variabilele (obiectele) declarate din cadrul programului

sunt de acest tip. Acestea sunt

- obiecte globale: se aloca în zona statică de memorie a programului

- obiecte cu vizibilitate locală: se alocă în zona de stivă a programului

- determinată la execuţie. Variabilele (obiectele) create dinamic sunt de acest tip.

Pentru acest tip de obiecte necesarul de memorie nu se poate determina în momentul

compilării. Aceste obiecte sunt alocate în zona de “heap” a programului. Pentru

crearea dinamică a unui asemenea obiect în C++ se foloseşte cuvântul cheie new iar

pentru distrugere delete.

Pentru obiectele alocate static sau pe stivă, compilatorul se ocupă de distrugerea acestora

la terminarea programului sau a unei zone de vizibilitate. Pentru obiectele dinamice,

programatorul trebuie să se ocupe de distrugerea acestora.

Unele limbaje obiectuale (ex. Java) au un mecanism prin care mediul de execuţie caută şi

identifică obiectele alocate dinamic care nu mai sunt utilizate în program, şi la

identificarea lor, le dezalocă. Mecanismul care realizează această caracteristică se

numeşte “garbage collector” şi degrevează programatorul de sarcina de a mai distruge

obiectele.

De multe ori, la execuţia programelor, pot apărea situaţii excepţionale, care nu au fost

prevăzute la scrierea programului şi care generează erori. Cu cât programatorul este mai

vigilent la scrierea programelor, cu atât mai mult scade probabilitatea ca programul

realizat să furnizeze erori. Unele limbaje avansate de programare furnizează mecanisme

Page 20: Silabus Limbaje si medii de programare

de evitare a erorilor, numite “exception handling”. Astfel, în momentul în care apare o

eroare, se generează un obiect numit excepţie care urmează să fie tratat în mod special de

program într-un fir alternativ de execuţie.

2.2. Analiza şi designul programelor orientate obiect

Paradigma programării obiectuale diferă substanţial de tipurile de programare anterioară,

în sensul că programele trebuie gândite într-un mod diferit. Există o metodă de realizare a

programelor orientate obiect. Metoda (metodologia) conţine un set de procese şi euristici

prin care se “sparge” complexitatea unei probleme de programare obiectuală.

În programarea obiectuală s-au dezvoltat multe metode orientate obiect de dezvoltare a

programelor. Înainte de a considera o asemenea metodă pentru rezolvarea unei probleme,

trebuie să se înţeleagă tipul problemei pentru rezolvarea căreia este destinată metoda.

Când citim şi dorim să înţelegem o metodă orientată obiect, trebuie să identificăm

răspunsul următoarelor întrebări:

- Care sunt obiectele?

- Care este interfaţa acestor obiecte?

Dacă se identifică obiectele şi interfeţele acestora, se poate începe cu scrierea

programelor.

Astfel, conform metodologiei obiectuale, procesul de realizare a programelor poate fi

descris în 5 paşi:

- Pasul 0: Se alege un plan care va fi urmat la rezolvarea problemei. Trebuie să se

decidă care sunt paşii pe care procesul analizat în conţine. Orice program se scrie cu

un scop precis. Trebuie să se identifice de ce este necesar programul, care sunt

obiectivele pe care acesta trebuie să le realizeze.

- Pasul 1: Se identifică problema de rezolvat. Acest pas corespunde cu scrierea

specificaţiilor programului. Trebuie identificat ceea ce urmează să realizeze

programul. Pentru descrierea funcţionalităţii programului, putem folosi diagramele

cazurilor de utilizare (use-case ). Acestea descriu răspunsul la următoarele întrebări:

- cine va folosi programul

- care sunt actorii din sistem

- ce fac aceşti actori în (cu) sistem(ul)

- cum ar evolua sistemul dacă actorii din sistem ar avea alte obiective decât cele

specifice problemei studiate

- ce probleme ar putea să apară (cazuri în care sistemul ar furniza erori)

Page 21: Silabus Limbaje si medii de programare

- Pasul 2: se răspunde la întrebarea: cum va fi construit sistemul? Acest pas furnizează

proiectarea claselor, precum şi a interacţiunilor dintre ele. Pentru fiecare clasa trebuie

identificate:

- numele clasei

- responsabilitatea clasei: ce funcţionalitate are clasa

- “colaborările” clasei: alte clase cu care clasa sub studiu interacţionează

- Pasul 3: se realizează nucleul sistemului. Presupune transcrierea designului furnizat

în pasul precedent în cod sursă, compilarea şi executarea programului. În acest pas

este important să se furnizeze un program funcţional, care se execută, chiar dacă acest

program este incomplet, nu realizează toate cerinţele problemei.

- Pasul 4: iterarea cazurilor de utilizare. Când avem un nucleu funcţional, urmează să

adăugam la acest nucleu funcţionalităţi din cazurile de utilizare a programului.

Fiecare nouă funcţionalitate adăugată programului constituie o iterare a programului,

respectiv o nouă versiune a aplicaţiei. Lungimea unei iteraţii depinde de

complexitatea aplicaţiei care se realizează. Se ajunge la sfârşitul iteraţiilor atunci când

se realizează toate cerinţele descrise în diagramele use-case, respectiv atunci când se

ajunge la termenul limită de livrare a programului sau când programul este acceptat

de client.

- Pasul 5. Utilizarea programului. În această fază se observă modul în care programul

răspunde utilizării curente. Se asigură mentenanţă programului, rezolvându-se

eventuale buguri (erori semnalate).

Analiza şi designul programelor orientate obiect reprezintă conţinutul principal al

disciplinelor din categoria “software engineering” (ingineria programării şi proiectare

obiectuală). În cadrul acestor discipline s-au dezvoltat metodologii concrete de lucru în

medii obiectuale. Intre acestea amintim OMT2 şi RUP3

În secţiunea următoare vom descrie stilul de programare „extreme programming” (XP),

având in vedere faptul că acesta permite o organizare eficientă a lucrului in echipe cu

puţini programatori, pe proiecte de dimensiuni rezonabile.

2.2.1. eXtreme Programming

eXtreme Programming (XP) reprezintă o filosofie alternativă pentru scrierea

programelor, respectiv un set de reguli pentru analiza şi realizarea programelor. Autorii

care au propus această alternativă o argumentează prin obţinerea unui câştig de 2 Object Management Technology, standardizată de OMG 3 Rational Unified Process, metodologie dezvoltată de Rational Software şi preluată de IBM

Page 22: Silabus Limbaje si medii de programare

productivitate respectiv de fiabilitate a activităţii de programare. XP se bazează pe 2

principii:

1. Write tests first: I mediat ce se obţine o bucată de cod care funcţionează,

programatorul trebuie să o testeze, pentru a se obţine siguranţa corectitudinii codului

respectiv. Adoptarea acestei tehnici presupune scrierea de programe de test pentru

fracţiuni de cod, înainte ca aceste bucăţi de cod să fie integrate în programul

principal. Aceasta are 2 consecinţe esenţiale:

- forţează o definire clară a interfeţei obiectelor

- testele sunt rulate ori de câte ori se realizează integrarea programelor.

2. Pair programming. Programarea trebuie realizată cu 2 programatori pe staţie de

lucru: una din persoane scrie codul iar cealaltă se gândeşte la cum trebuie să arate

codul. De obicei, cel care gândeşte păstrează întotdeauna o vedere de ansamblu

asupra aplicaţiei care trebuie realizată, nu numai asupra piesei de cod care se scrie la

un moment dat. “Pair programming” este utilizat în firmele de software prin

asigurarea unui analist sau a unui “senior programmer” la o echipa de programatori.

Programatorul senior are viziune asupra aplicaţiei scrise, cunoaşte detaliile de design

ale aplicaţiei, şi intră în detaliile de implementare doar atunci când un programator

membru al echipei nu poate realiza o anume funcţionalitate.

Recomandăm utilizarea acestui stil de programare în activitatea de învăţare a programării

obiectuale. Astfel, totdeauna, programatorii vor putea să se corecteze unul pe celălalt. Se

va asigura astfel deprinderea mai rapidă a cunoştinţelor necesare programării, respectiv

corectitudinea codului rezultat.

2.3. Formalizarea conceptului de obiect

În C, o structură reprezintă o aglomerare de date, un mod de a împacheta mai multe

variabile pentru o utilizare comună a lor. În C++, un obiect este o variabilă, adică un

spaţiu de memorie care are un identificator unic, care păstrează date şi specifică operaţiile

care se pot executa asupra acestor date.

Se numeşte încapsulare abilitatea de a împacheta împreună date cu funcţii, în vederea

creării de noi tipuri de date. Astfel, prin definiţia lui CppVector s-a creat un nou tip de

date. Ne referim la tipurile de date noi create prin sintagma de “tipuri abstracte de date”

deoarece ele permit abstractizarea unor concepte din spaţiul problemei de rezolvat.

Sintagma

obiect.functieMembra(listaArgumente)

Page 23: Silabus Limbaje si medii de programare

înseamnă pentru compilator apelarea funcţiei membru pentru un obiect, iar în

terminologie orientată obiect înseamnă transmiterea unui mesaj către obiectul respectiv.

Astfel, un program în C++ înseamnă creare de obiecte şi transmitere de mesaje către

acestea.

Mărimea unei structuri este egală cu mărimea adunată a tuturor membrilor structurii.

Uneori, compilatorul adaugă câţiva octeţi pentru a stoca informaţii adiţionale despre

obiecte. Mărimea unui obiect poate fi identificată prin sizeof.

La crearea de noi tipuri de date se doreşte separarea interfeţei de implementare. Astfel,

declararea tipului de date se va realiza în fişiere header, iar implementarea (definirea

funcţiilor) în fişiere cpp. Astfel, în programe mai complexe, vor putea schimba

implementarea specifică anumitor tipuri de date fără a fi nevoiţi să schimbăm restul

sistemului. Fişierele header sunt obligatorii pentru dezvoltarea programelor în C++.

Fişierele header realizează contactul între programatorii de biblioteci şi utilizatorii

acestora.

În acest sens, putem enunţa o definiţie informală de care se ţine cont în activitatea de

programare obiectuală:

C++ one definition rule: pentru fiecare obiect C++ este permisa o singură definiţie.

Astfel, în fişierele header trebuie plasate doar declaraţii, în timp ce definiţiile se vor plasa

în fişierele cpp.

2.4. Ascunderea implementării

Programarea orientată obiect introduce controlul accesului la membrii unei structuri.

Acesta este necesar deoarece:

- programatorul client nu trebuie să poată accesa funcţionalităţi care nu îi sunt destinate

(cum ar fi manipulările interne structurii)

- programatorul bibliotecii de obiecte trebuie să poată schimba implementarea internă

fără să afecteze eventualele programe client scrise deja, care utilizează biblioteca

2.4.1. Specificatori de acces

În C++ specificatorii de acces sunt utilizaţi în declaraţii de structuri, şi sunt urmaţi de :.

Aceştia sunt:

Page 24: Silabus Limbaje si medii de programare

- public: declaraţiile care urmează acestui specificator sunt disponibile pentru oricine.

Membrii public sunt identici cu membrii necalificaţi ai unei structuri.

- private: declaraţiile care urmează acestui specificator sunt disponibile doar în

funcţiile membre ale tipului respectiv. Dacă se încearcă să se acceseze un membru

private, compilatorul generează eroare.

- protected: este similar cu private; are semnificaţie doar în contextul utilizării

moştenirii. Astfel, clasele care moştenesc din tipul în discuţie au acces la membrii

protected. Membrii protected nu se văd în afara ierarhiei de moştenire.

Specificatorii pot să apară în orice număr şi ordine în cadrul unei structuri (clase). Aceşti

specificatori de access au rolul de a ascunde implementările de interfeţe, şi de a asigura

un control eficient şi o separare a datelor şi conceptelor din program.

2.4.2. Încapsulare

Controlul accesului mai este denumit şi “ascunderea implementării”. Încapsularea şi

controlul accesului conduce la definirea unor entităţi care sunt mai mult decât simple

structuri C. Astfel, prin încapsulare, putem cuprinde împreună date (proprietăţi) şi funcţii

(comportament). Prin controlul accesului putem separa interfaţa de implementare. Astfel,

se poate schimba în orice moment implementarea fără a afecta funcţionalitatea

programelor care utilizează variabile de tipul respectiv (ele interacţionează doar prin

intermediul interfeţei). Astfel, apare noţiunea de clasă, definită în C++ prin cuvântul

cheie class. O clasă este o structură, cu menţiunea că toţi membrii acesteia sunt implicit

membrii privaţi, spre deosebire de struct unde implicit toţi membrii sunt publici.

2.5. Construirea şi distrugerea obiectelor. Supraîncărcare

2.5.1. Constructori şi destructor

Un număr mare de erori C provin din faptul că programatorii uită să iniţializeze

variabilele, respectiv să dezaloce memoria la sfârşitul execuţiei programului. Aceste erori

sunt amplificate la utilizarea bibliotecilor, când programatorii clienţi nu ştiu cum să

iniţializeze datele din structuri, fie din ignoranţă, fie din lipsa unor documentaţii adecvate

a bibliotecilor. Constructorii vin să rezolve această problemă

Dacă o clasă are un constructor, compilatorul invocă în mod automat constructorul la

crearea obiectului. Constructorul este o funcţie membră a clasei care are acelaşi nume ca

şi clasa. Astfel, la definirea clasei avem următoarea declaraţie de funcţie membru:

Page 25: Silabus Limbaje si medii de programare

Ca şi orice altă funcţie, constructorul poate primi argumente, şi anume, elemente care să

controleze modul în care este iniţializat obiectul. În plus, se pot scrie mai mulţi

constructori şi apelul acestora se poate realiza în mai multe modalităţi.

Constructorii (şi destructorii) nu au tip de return. Aceasta înseamnă că tipul de return

diferă de void (care înseamnă că funcţia nu returnează nimic). Dacă constructorii ar avea

tip de return (fie şi void) atunci ar fi nevoie ca să se realizeze apelul explicit al acestora,

deci constructorii nu s-ar mai putea apela implicit.

Destructorii sunt funcţii speciale membre ale claselor care se execută pentru a realiza

“ştergerea” (distrugerea) obiectelor. Numele destructorului este identic cu numele

claselor, fiind precedat de particula ~. Destructorii nu au argumente şi nici tip de return.

Pentru variabilele alocate pe stivă, destructorul este apelat automat când obiectul iese din

domeniul de vizibilitate. Aceasta se întâmplă chiar şi atunci când se foloseşte saltul

automat (goto) pentru a se ieşi dintr-un domeniu de vizibilitate.

Pentru variabilele dinamice, alocate in zona de heap, destructorul este apelat la

distrugerea acestor variabile prin operatorul delete.

În C, variabilele trebuiau definite întotdeauna la începutul blocului de vizibilitate (de ex.

la începutul funcţiilor). Această regulă nu se mai păstrează în C++. Astfel, obiectele (şi

variabilele) pot fi definite oriunde în cod şi definirea acestora se face de regulă, cât mai

aproape de locul de utilizare.

În C++, nu se permite crearea unui obiect până când constructorul acestuia nu va avea

disponibile toate informaţiile pentru apelul său (argumentele constructorului trebuie să fie

definite şi iniţializate).

Cu toate că putem defini variabile în orice loc în cod, alocarea memoriei pentru

variabilele respective se realizează la începutul domeniului de vizibilitate în care apare

variabila. Variabila respectivă va fi disponibilă pentru utilizare doar după ce se întâlneşte

locul de definiţie, când se realizează şi apelul constructorului. Compilatorul va verifica

inclusiv faptul ca definiţia obiectului trebuie să se realizeze, deci constructorul trebuie să

se poată apela la locul de definire al obiectului.

Se numeşte constructor implicit (default), constructorul care nu are argumente.

În cazul în care clasa are constructor, compilatorul va forţa crearea obiectelor din clasa

respectivă prin intermediul constructorului. In acest caz, este absolute necesar să se

folosească in mod explicit constructorii la crearea obiectelor.

Dacă clasa nu are nici un constructor, atunci compilatorul va scrie (crea) automat un

constructor implicit pentru clasa respectivă. Cu toate că se creează în mod automat un

constructor implicit pentru obiect (în cazul în care clasa nu are constructori),

comportamentul acestui constructor este nedefinit. În consecinţă, se recomandă scrierea

Page 26: Silabus Limbaje si medii de programare

de constructori pentru clase, şi în mod special, definirea explicită a constructorului

implicit.

C++ garantează crearea obiectelor la nivelul unei unităţi de compilare (funcţie), în

ordinea în care acestea sunt scrie. Distrugerea obiectelor se realizează în ordine inversă

creării.

Constructorul de copiere este constructorul clasei care primeşte ca şi argument o referinţă

la un obiect din tipul clasei respective. Astfel, constructorul de copiere creează obiectul

pe baza unui alt obiect de acelaşi tip.

Constructorul de copiere este esenţial la transmiterea prin valoare a obiectelor din tipuri

(clase) definite de utilizator. În cazul în care programatorul nu scrie un constructor de

copiere în clasă, compilatorul va genera în mod automat un asemenea constructor.

La transmiterea prin valoare a unui obiect unei funcţii , funcţiei îi este transmisă o copie a

obiectului. Copia obiectului este obţinută prin apelarea constructorului de copiere În

cazul în care obiectul este de dimensiune mare, procesul de creare a copiilor transmise,

respectiv de returnare a unor asemenea obiecte din funcţii devine costisitor (din punct de

vedere al timpului de execuţie şi al memoriei consumate).

Constructorul de copiere este utilizat în special la transmiterea prin valoare. O alternativă

la constructorul de copiere este evitarea transmiterii prin valoare. Pentru a se evita

transmiterea prin valoare, constructorul de copiere trebuie declarat ca şi membru privat al

clasei. În acest mod doar funcţiile care sunt declarate friend în clasa respectivă vor putea

realiza transmiterea prin valoare.

În cazul în care nu există nici o funcţie care realizează transmitere prin valoare, definiţia

funcţiei membru constructor de copiere poate lipsi.

O altă modalitate de evitare a constructorului de copiere este înlocuirea transmiterii prin

valoare prin transmiterea de referinţe constante

2.5.2. Supraîncărcarea funcţiilor

2.5.2.1. Conceptul de supraîncărcare

Funcţia reprezintă un nume asociat unei acţiuni (sau unui sir de acţiuni). De multe ori

apare o problemă la modelarea spaţiului problemei, când acelaşi nume are semnificaţii

diferite, în funcţie de contextul de apel. Conceptul are mai multe înţelesuri, este deci

“supraîncărcat”.

În multe limbaje de programare (inclusiv C) se solicită utilizarea de nume diferite pentru

sensuri diferite, sau în altă formulare, nume unice pentru fiecare înţeles distinct. În C++,

Page 27: Silabus Limbaje si medii de programare

această regulă nu se mai păstrează. Un exemplu de încălcare a ei este utilizarea aceluiaşi

identificator pentru numele clasei şi numele constructorului.

Supraîncărcarea funcţiilor permite folosirea aceluiaşi identificator pentru a denumi mai

multe funcţii. La supraîncărcarea funcţiilor, se poate folosi acelaşi nume, dar lista

argumentelor trebuie să difere.

Nu se poate realiza supraîncărcarea funcţiilor numai pe tipul returnat.

Deoarece constructorii sunt la rândul lor funcţii, ei se pot supraîncărca. Astfel, se pot

scrie mai mulţi constructori pentru o clasă.

2.5.2.2. Supraîncărcarea operatorilor

Supraîncărcarea operatorilor reprezintă o nouă modalitate de apel a funcţiilor, existând o

diferenţă doar în ceea ce priveşte modul de scriere a apelului. Astfel, argumentele

funcţiilor nu mai apar între paranteze rotunde ci se scriu în jurul unor operatori, la fel ca

la scrierea unor expresii care conţin operatori.

C++ permite definirea unor operatori care să funcţioneze în conjuncţie cu clasele.

Definirea acestor operatori este similară cu definirea unor funcţii membre. Pe lângă

modalitatea de suprascriere a operatorilor prin funcţii membre, există şi posibilitatea de a

defini operatorii ca funcţii globale.

Definirea unui operator supraîncărcat este similară cu definirea unei funcţii membru, cu

diferenţa că numele funcţiei este operator@ unde @ reprezintă operatorul care se

supraîncarcă.

Supraîncărcarea se poate realiza şi prin funcţii globale. În acest caz, funcţia

supraîncărcată poate fi declarată ca şi friend în clasa pentru care se realizează

supraîncărcarea.

La supraîncărcarea operatorilor se aplică următoarea regulă:

- În cazul în care se supraîncarcă un operator ca şi funcţie membru şi operatorul este

binar, atunci funcţia supraîncărcată are un singur argument.

- În cazul în care se supraîncarcă un operator ca şi funcţie membru şi operatorul este

unar, atunci funcţia supraîncărcată nu are argumente.

- În cazul în care se supraîncarcă un operator ca şi funcţie globală şi operatorul este

binar, atunci funcţia supraîncărcată are 2 argumente.

- În cazul în care se supraîncarcă un operator ca şi funcţie globală şi operatorul este

unar, atunci funcţia supraîncărcată are un singur argument.

La supraîncărcarea operatorilor, trebuie să se ţină cont de următoarele directive:

- nu se pot supraîncărca operatori care nu au sens în C (de exemplu operatorul ** - nu

există în C++)

Page 28: Silabus Limbaje si medii de programare

- prin supraincărcare nu se poate schimba precedenţa operatorilor. Aceasta rămâne cea

definită de limbajul C++

- prin supraîncărcare nu se poate schimba numărul operanzilor ceruţi de un operator.

Astfel, un operator C++ binar nu poate fi rescris ca şi un operator unar.

Se prezintă următoarele caracteristici ale supraîncărcării operatorilor în C++:

- operatorul ++ poate fi apelat atât în forma prefixată cât şi în forma postfixată. Pentru

a supraîncărca operatorul ++ în forma prefixată, ca şi funcţie membru, se scrie funcţia

în mod obişnuit. Pentru a supraîncărca ++ ca şi operator postfixat, se scrie funcţia

membru având o un argument “dummy” (care nu se foloseşte niciodată). Astfel, cele

2 funcţii vor avea semnături diferite şi compilatorul poate să discearnă între ele.

- pentru a supraîncărca operatorul ++ în forma prefixată, ca şi funcţie globală, se scrie o

funcţie globală obişnuită, cu un singur argument. Pentru a supraîncărca operatorul ++

în forma postfixată ca şi funcţie globală, se scrie funcţia globală având un argument

“dummy”. Astfel, cele 2 funcţii vor avea semnături diferite şi compilatorul poate să

discearnă între ele.

- regulile de mai sus se aplică pentru supraîncărcarea oricăror operatori unari, la care

există forme postfixate şi prefixate.

Operatorul de atribuire poate fi suprascris doar ca şi funcţie membră. La operatorul de

atribuire se recomandă verificarea realizării atribuirii între aceleaşi 2 obiecte (a=a).

Atribuirea între aceleaşi 2 obiecte de obicei nu are sens.

Următoarele reguli se aplică la transmiterea argumentelor şi returnarea valorilor:

- dacă operatorul supraîncărcat doar citeşte valorile operanzilor, argumentele se vor

transmite prin referinţe (&) constante (ex. operatorul +). În cazul în care valoarea

unui operand se modifică, acel operand nu va mai fi transmis prin referinţă constantă.

- tipul valorii returnate depinde de semnificaţia operatorului. Dacă operatorul produce

o valoare nouă, atunci aceasta trebuie returnată prin valoare (şi nu prin referinţă).

Astfel, la operatorul + se returnează un Integer constant (creat prin apelarea

constructorului).

- operatorii de atribuire trebuie să poată permite modificarea valorii returnate (pentru a

permite a=b=c). Astfel, se vor returna referinţe (neconstante). Referinţele neconstante

permit utilizarea rezultatului atribuirii ca şi operanzi stânga în expresii (ex.

(a=b).func(); )

- operatorii booleeni vor returna valori int şi nu bool

Din considerente de siguranţă, pentru a nu încălca principiile de verificării de tip impuse

de C++, unii operatori nu se pot supraîncărca. Regulile următoare se aplică în ceea ce

priveşte restricţiile la supraîncărcarea operatorilor:

Page 29: Silabus Limbaje si medii de programare

- nu se poate supraîncărca operatorul . Dacă operatorul . ar putea fi supraîncărcat, nu

ar mai exista posibilitatea de selecţie a membrilor unei clase.

- nu se poate supraîncărca operatorul .*

- nu există operator pentru operaţia exponent.

- nu se pot inventa noi operatori faţă de cei pe care C ii pune la dispoziţie

prin supraîncărcare nu se poate schimba precedenta operatorilor

2.5.2.3. Considerente de design la supraîncărcarea operatorilor

La designul claselor, apare întrebarea referitoare la modul în care trebuie supraîncărcaţi

operatorii, adică la alegerea între supraîncărcarea operatorilor prin funcţii membre sau

prin funcţii globale.

De obicei, dacă nu există vreo cerinţă expresă, se alege supraîncărcarea prin funcţie

membră. Supraîncărcarea operatorilor prin funcţii membre accentuează asocierea dintre

obiect şi operatorul care i se aplică acestuia. Când obiectul apare ca şi operand stânga în

expresie, relativ la operator, atunci această abordare e cea mai potrivită.

Apar cazuri când operandul din stânga este dintr-o altă clasă decât cea în care ar trebui

supraîncărcat operatorul. În acest caz, cel mai uzual este utilizarea funcţiilor globale

pentru suprascrierea operatorilor.

2.5.2.4. Supraîncărcarea operatorului de atribuire

Operatorul de atribuire este un operator special care trebuie tratat special. Operatorul de

atribuire este cumva similar constructorului de copiere. Astfel, el realizează copierea

conţinutului obiectului din dreapta în zona de memorie a obiectului din stânga atribuirii.

Operatorul de atribuire trebuie suprascris ca şi funcţie membră. În cadrul funcţiei, trebuie

copiat conţinutul obiectului din dreapta (cel transmis ca şi argument) în obiectul din

stânga (cel asupra căruia se realizează apelul funcţiei). La suprascrierea operatorului de

atribuire trebuie eliminat cazul de auto-atribuire. Aceasta se realizează prin inserarea unei

verificări înainte de realizarea copierii membrilor.

Realizarea copierii membrilor e mai dificilă atunci când obiectele conţin pointeri. Simpla

copiere a valorii pointerilor ar face ca acelaşi obiect membru să fie referit de 2 pointeri.

Distrugerea unuia dintre cele 2 obiecte ar realiza dezalocarea obiectelor referite, şi ar

introduce buguri în program. Acest comportament a putut fi observat şi în cazul în care

constructorul de copiere nu ţine cont de acest lucru.

În concluzie, se impune o abordare diferită în cazul atribuirii între obiecte care agregă

indirect (prin pointeri) alte obiecte.

Page 30: Silabus Limbaje si medii de programare

O primă abordare presupune copierea a tot ce se află în spatele pointerilor membrii., în

zonele de memorie aferente obiectului din stânga atribuirii. Această abordare trebuie

considerată atât pentru operatorul de atribuire cât şi pentru constructorul de copiere.

Constructorul de copiere şi operatorul de atribuire duplică obiectul din spatele pointerului

membru. Din momentul apelării constructorului de copiere sau a operatorului de atribuire

cele 2 obiecte sunt automat separate unul de celălalt.

A doua abordare care poate fi considerată la scrierea constructorului de copiere sau la

supraîncărcarea operatorului de atribuire în cazul obiectelor cu pointeri este numărarea

referinţelor. Aceasta se aplică atunci când obiectele nou create necesită un spaţiu de

memorie mare sau când crearea de noi obiecte este dificil ă şi costisitoare.

Numărarea referinţelor se realizează la nivelul obiectelor agregate indirect (prin pointeri).

Acestea vor conţine un contor care va număra de câte ori e referit obiectul (de către alţi

pointeri). La fiecare ataşare de pointer către obiectul respectiv (adică apel de constructor

de copiere sau asignare în clasa superioară), contorul se va incrementa; la fiecare

distrugere de pointer (apelare de destructor pentru clasa superioară), contorul se va

decrementa. În acest caz, acelaşi obiect va fi referit de pointeri aparţinând de obiecte

diferite.

Problema care apare în această abordare este la scrierea de date în obiectul referit. Dacă

contorul obiectului referit indică o valoare mai mare ca şi 1, înseamnă că mai multe

obiecte referă obiectul în cauză iar scrierea va modifica datele altui obiect. Astfel,

scrierea va fi permisă doar în cazul în care obiectul agregat este referit doar o singură

dată. În cazul în care dorim să scriem şi în cazul general (când numărul de referiri este

mai mare decât 1), atunci va trebui să duplicăm obiectul referit, şi să facem scrierea doar

pe copia nou creată.

Similar ca şi la constructorul de copiere, în cazul în care programatorul nu supraîncarcă

operatorul de atribuire compilatorul va genera un astfel de operator în mod implicit.

Operatorul de atribuire creat implicit are acelaşi comportament ca şi constructorul de

copiere creat implicit. La apelarea operatorului de atribuire creat implicit, se asigură

apelarea operatorilor de atribuire pentru membrii clasei respective.

2.6. Gestiunea dinamică a memoriei

2.6.1. Crearea obiectelor

La crearea unui obiect C++, se realizează următoarele acţiuni:

1. se alocă memorie pentru obiect

Page 31: Silabus Limbaje si medii de programare

2. se apelează un constructor pentru iniţializarea zonei de memorie alocate în pasul 1.

Aceste 2 acţiuni se realizează întotdeauna la crearea obiectelor în C++. Alocarea

memoriei se poate realiza în următoarele moduri:

1. zona de memorie poate fi alocată înainte de începerea execuţiei programului. Aceste

zone se alocă în zona statică de memorie, şi ele există pe toată durata execuţiei

programului.

2. zona de memorie poate fi alocată pe stivă, în momentul în care execuţia programului

ajunge la un început de domeniu de vizibilitate. Această zonă alocată este automat

eliberată în momentul in care programul ajunge la sfârşitul domeniului de vizibilitate.

Aceste operaţii de alocare - dezalocare de memorie sunt gestionate de compilator şi

ele sunt foarte eficiente. Programul însă, trebuie să cunoască la momentul compilării,

numărul şi mărimea acestor variabile.

3. zona de memorie poate fi alocată în zona de heap. Alocarea de memorie în zona de

heap se realizează la execuţia programului şi se numeşte alocare dinamică. Alocarea

dinamică se realizează prin intermediul unor funcţii cărora li se transmite necesarul de

memorie de alocat. Programatorul este cel care controlează mărimea memoriei astfel

alocate precum şi momentul alocării. Dezalocarea este, de asemenea, lăsată la decizia

programatorului. Domeniul de viaţă a acestor variabile dinamice este determinat de

programator şi nu intră sub incidenţa unor reguli de limbaj.

2.6.2. Crearea dinamică a obiectelor în C şi C++

C furnizează 2 funcţii de bază pentru alocare şi dezalocare de memorie: malloc şi free.

Aceste funcţii sunt practice dar destul de primitive, în sensul că programatorul trebuie să

cunoască bine mecanismul de alocare / dezalocare a memoriei dinamice pentru a le putea

folosi. La apel funcţia malloc trebuie să primească la intrare mărimea zonei de memorie

care se doreşte alocată şi returnează un pointer de tip void*. Deci malloc nu returnează o

zonă de memorie aferentă unui tip anume, iar în acest caz programatorul trebuie să

convertească acest pointer spre tipul dorit. După alocarea memoriei cu malloc

programatorul trebuie să se ocupe în mod explicit de iniţializarea zonei de memorie. C nu

asigură o iniţializare automată. La dezalocare, apelul funcţiei free trebuie precedat de

apelarea unor destructori care să efectueze operaţii uzuale de ştergere a obiectului.

Efortul de programare solicitat de limbajul C pentru utilizarea alocării dinamice a

memoriei este destul de mare, iar mecanismele de gestiune a memoriei sunt greoaie. De

acea mulţi programatori evită pe cât posibil să folosească alocarea dinamică a memoriei,

utilizând mecanisme alternative mult mai ineficiente.

Page 32: Silabus Limbaje si medii de programare

C++ a combinat acţiunile necesare creării dinamice a unui obiect în apelul unui singur

operator: new. Acesta asigură alocarea memoriei necesare obiectului în zona de heap şi

iniţializarea obiectului nou creat prin apelarea unui constructor. new poate fi utilizat pentru

crearea obiectelor folosind oricare din constructorii definiţi pentru o clasa anume.

Operatorul delete este complementar operatorului new. Acest operator asigură mai întâi

apelarea destructorului şi apoi eliberarea memoriei. delete poate fi apelat doar pentru

eliberarea memoriei alocate prin new. Nu se poate apela delete pentru dezalocarea

memoriei alocate prin malloc. La ştergerea prin delete a unui pointer care indică valoarea

0 (NULL) practic, nu se întâmplă nimic, de acea se obişnuieşte setarea unui pointer cu

valoarea NULL imediat după ce a fost dezalocat cu delete.

2.6.3. Managementul memoriei la execuţie

La crearea obiectelor la momentul compilării, mărimea şi durata de viaţă a obiectelor este

strict determinată. Astfel, compilatorul “ştie” în ce moment şi unde (locaţia exactă de pe

stivă) să aloce aceste obiecte. El va gestiona stiva într-un asemenea mod astfel încât

programul să poată rula cu succes. Această gestiune a memoriei (a stivei) este realizată în

momentul compilării / link-editării, atunci când se stabilesc locaţiile şi momentele unde

respective când se vor aloca obiectele.

În cazul alocării dinamice a memoriei, există o încărcare suplimentară a programului

pentru a determina locaţia unde să se aloce memorie pentru o variabilă dinamică. La o

cerere de alocare, (malloc), funcţia care implementează această alocare de memorie

trebuie să caute un bloc de memorie liberă suficient de mare, pentru a satisface nevoia de

alocare şi, înainte să returneze pointerul către zona de memorie alocată, trebuie să

marcheze această zonă ca fiind ocupată într-o hartă a memoriei. Toate aceste proceduri

necesare a fi executate la alocarea dinamică sunt implementate în cadrul unei strategii

numite “managementul memoriei”, şi reprezintă o încărcare suplimentară pentru sistem.

Diverse compilatoare de C/C++ realizează managementul memoriei în mod diferit, însă

rezultatul acestor funcţii trebuie să fie acelaşi: să corespundă standardului C/C++.

Multe limbaje avansate de programare tratează în mod diferit problema gestiunii

dinamice a memoriei. Astfel, se pune la dispoziţia programatorului mecanismul denumit

garbage collector care adună şi dezalocă toată memoria alocată dinamic dar rămasă

agăţată, fără posibilitatea de a mai fi referită. Mediile de programare Java şi .Net

furnizează asemenea mecaniste. În prezenţa garbage collector-ului, programatorul nu mai

trebuie să se preocupe de dezalocarea memoriei alocate dinamic, deoarece sistemul va

asigura în mod automat acest lucru.

Page 33: Silabus Limbaje si medii de programare

La apelarea operatorului new se alocă memorie pentru obiect şi apoi se apelează

constructorul. La apelul operatorului delete se apelează destructorul şi apoi se dezalocă

memoria. Prin suprascrierea operatorilor new şi delete se poate schimba modul de alocare

respectiv dezalocare a memoriei, însă mecanismele de apel ale constructorului şi

destructorului rămâne neschimbate.

Mecanismul de alocare respectiv dezalocare a memoriei oferite de new şi delete este unul

de utilizare generală. În majoritatea cazurilor acesta satisface nevoile programelor

noastre. Unul din motivele care justifică rescrierea acestor operatori este cel de eficienţă.

Supraîncărcarea operatorilor new şi delete permite implementarea unui mecanism de

alocare respectiv dezalocare particular, propriu cerinţelor problemelor concrete.

În cele ce urmează dăm exemple de probleme concrete care necesită supraîncărcarea

acestor operatori:

- existenţa unei clase la care frecvenţa creării obiectelor să fie foarte mare. În acest caz,

apar probleme de rapiditate a apelurilor operatorului new

- existenţa unei frecvenţe mari a alocării memoriei pentru obiecte de dimensiuni

diferite. În acest caz, se poate ajunge la o fragmentare mare a memoriei, care să nu

mai permită alocări de obiecte mari, chiar dacă memorie liberă (necontiguă) există.

- probleme la alocare apar de obicei în sisteme în timp real care lucrează cu resurse

limitate pentru perioade lungi de timp. În astfel de sisteme alocarea memoriei trebuie

să dureze un timp foarte scurt şi de obicei, constant, iar probleme de epuizare a

memoriei trebuie excluse.

La supraînărcarea operatorului new când un obiect este creat prin intermediul noului

operator, compilatorul asigură următoarea funcţionalitate:

- se alocă spaţiul de memorie folosind noul operator new

- se apelează constructorul

La supraîncărcarea operatorului delete, când un obiect este distrus prin intermediul noului

operator, compilatorul asigură următoarea funcţionalitate:

- se apelează destructorul

- se dezalocă spaţiul de memorie folosind noul operator delete.

Trebuie ţinut cont că la rescrierea acestor operatori trebuie furnizat (rescris) şi

mecanismul de management al memoriei în cazul în care aceasta se epuizează.

Page 34: Silabus Limbaje si medii de programare

2.7. Programarea cu obiecte din mai multe clase

2.7.1. Compoziţia

Compoziţie înseamnă a crea o noua clasă care conţine ca şi date membre obiecte din clase

existente. Se spune că un obiect din noua clasă agregă obiecte din clasele vechi

(existente).

Compoziţia (agregarea) este de două tipuri:

- directă: atunci când obiectul membru este în mod efectiv parte componentă a

obiectului din noua clasa

- indirectă: atunci când obiectul din noua clasă referă (prin intermediul unui pointer) un

obiect din clasa existentă

În descriere UML, structura de clase de mai sus poate fi reprezentată prin figura 79:

+X()+set(in ii : int) : void+read() : int+permute() : int

-i : int

X

+Y()+f(in ii : int) : void+g() : int+permute() : void

-i : int-x : X

Y

11

Figura 19.2 Reprezentarea grafică a agregării

Rombul plin înseamnă agregare directă. Agregarea este expresia unei relaţii de tipul

“has-a”, şi se traduce prin expresia: un obiect din tipul X este membru al unui obiect din

tipul Y, sau un obiect din tipul Y se compune dintr-un obiect din tipul X.

2.7.2. Moştenirea

Prin moştenire se creează o clasă nouă (clasa derivată) care se aseamănă cu o clasă

existentă (clasa de bază). Moştenirea este expresia unei relaţii de tipul “is-like-a”.

Sintaxa moştenirii presupune înşiruirea claselor de bază după numele clasei care se

creează, înainte de a se trece la corpul definiţiei noii clase. C++ permite moştenirea

multiplă, adică derivarea unei clase utilizând mai multe clase de bază.

Prin moştenire noua clasă va avea ca şi membrii toţi membrii clasei de bază. În plus, noua

clasă poate să îşi definească proprii membrii, sau să rescrie definiţiile membrilor din clasa

de bază. Subsetul de membrii din clasa de bază care vor fi moşteniţi în clasa derivată se

determină utilizând regulile de combinare a specificatorilor de acces.

În rescriere UML, structura de clase din exemplul de mai sus este reprezentată în figura

19.3.

Page 35: Silabus Limbaje si medii de programare

+X()+set(in ii : int) : void+read() : int+permute() : int

-i : int

X

+Y()+change() : int+set(in ii : int) : void

-i : int

Y

Figura 19.3 Reprezentarea grafică a moştenirii

Noua clasă Y moşteneşte toate datele membre ale lui X. De fapt Y conţine un subobiect

de tipul X ca şi cum am fi realizat agregarea tipul X în tipul Y. Astfel, la apelul sizeof se

remarcă faptul că Y ocupă mai mult spaţiu decât X. Membrii privaţi ai lui X există în

continuare ca şi membrii privaţi ai subobiectului, însă mecanismul private funcţionează,

adică nu putem accesa aceşti membrii din afara obiectului de care aparţin (care este de tip

X). Deci, Y moşteneşte doar membrii din interfaţa lui X, la care se poate avea acces din

afară.

Se observă modul în care metoda set din clasa Y redefineşte metoda set din clasa X. In

acest caz, nu e vorba de supraîncărcare, deoarece semnăturile celor 2 funcţii (cea din

clasa X şi cea din clasa Y) sunt identice. La invocarea metodei set pe un obiect din clasa

Y se va apela metoda set redefinită.

Se observă utilizarea cuvântului cheie public înainte de numele clasei de bază. Dacă s-ar

omite această specificare, în mod implicit, moştenirea funcţionează ca şi privată, adică,

toţi membrii publici ai clasei de bază s-ar moşteni ca şi membrii privaţi în clasa derivată.

Prin folosirea cuvântului cheie public, toţi membrii publici din clasa de bază se

moştenesc ca şi membrii publici în clasa derivată.

2.7.3. Combinarea compoziţiei cu moştenirea

Compoziţia şi moştenirea pot fi combinate la libera alegere a programatorului. Astfel, se

pot crea sisteme oricât de complexe şi extensibile. Figura de mai jos prezintă un exemplu

de combinare a agregării cu moştenirea.

Page 36: Silabus Limbaje si medii de programare

+A()+~A()+f() : void

-i : int

A

+B()+~B()+f() : void

-i : int

B

+C()+~C()+f() : void

-a : A

C11

Figura 19.4 Agregare şi moştenire combinate

Se observă că de multe ori, la crearea obiectelor complexe este nevoie de apel explicit al

constructorilor.

Apelul destructorilor acestor obiecte se realizează în mod automat, de către compilator.

Secţiunea următoare va trata în detaliu ordinea de apel automat a constructorilor şi

destructorilor, în cazul combinării compoziţiei cu moştenirea.

2.7.4. Ini ţializarea obiectelor în cazul folosirii compoziţiei şi moştenirii

La crearea unui obiect, compilatorul C++ garantează apelarea constructorilor pentru toate

obiectele membre ale obiectului respectiv. Aceasta înseamnă apelarea constructorului

implicit pentru sub-obiecte.

Problema apare în momentul în care clasele agregate nu au definiţi constructori implicit,

sau atunci când la iniţializare dorim să transmitem un alt argument în locul argumentului

implicit. Această problemă este rezolvată prin utilizarea listei de iniţializare.

În cazul moştenirii, utilizarea listei de iniţializare presupune apelarea explicită a

constructorului clasei de bază.

Constructorii obiectelor agregate trebuie să fie apelaţi înainte de a se trece la execuţia

codului din corpul constructorului clasei curente. În momentul în care se execută corpul

constructorului curent, obiectele membre trebuie să fie deja iniţializate.

Regula de apelare a constructorilor şi destructorilor în cazul utilizării combinate a

agregării şi moştenirii este prezentată în cele ce urmează:

Mai întâi se apelează constructorul clasei de bază, apoi se apelează constructorii

obiectelor agregate, în ordinea in care apar in definiţia clasei.

Ordinea constructorilor în lista de iniţializare nu afectează ordinea în care sunt apelaţi

constructorii. Pentru fiecare apel de constructor, această regulă se aplică întocmai.

Destructorii se apelează întotdeauna în ordine inversă a apelului constructorilor, pentru

aceleaşi obiecte.

Page 37: Silabus Limbaje si medii de programare

2.7.5. Ascunderea numelor prin moştenire

În general, se poate spune că ori de câte ori într-o clasă derivată se redefineşte o funcţie

supraîncărcată în clasa de bază, funcţiile supraîncărcate din clasa de bază devin ascunse.

Se consideră că prin moştenire se importă interfaţa clasei de bază în clasa derivată.

Astfel, dacă clasa de bază suportă polimorfismul prin supraîncărcarea funcţiilor, atunci

această facilitate rămâne prin utilizarea moştenirii. Dacă, în plus, în clasa derivată, se

schimbă definiţia uneia din funcţiile importate care intră sub incidenţa polimorfismului,

înseamnă că se doreşte schimbarea interfeţei moştenite din clasa de bază, deci aceasta nu

se mai moşteneşte deloc in clasa derivată.

Redefinirea funcţiilor membre ale claselor de bază este utilă la specializarea colecţiilor

generice. De multe ori este nesigur să returnăm pointeri void, deoarece aceştia pot fi

convertiţi de către programul client către orice tip dorit, fără să fim atenţionaţi în vreun

fel de compilator.

Soluţia este să specializăm clasa colecţie generică pentru un anume tip de elemente.

Astfel, vom moşteni din clasa generică o nouă clasă care va avea redefinite funcţiile din

interfaţă pentru a suporta un anume tip specific.

Problema acestei abordări este că prin specializare schimbăm în mod drastic interfaţa

clasei de bază. Se pune problema utilităţii moştenirii în acest caz.

La realizarea operaţiei de moştenire, următoarele funcţii membre nu se moştenesc din

clasa se bază în clasa derivată:

- constructorii şi destructorii

- operatorul de atribuire, deoarece realizează o operaţie similară constructorului de

copiere

2.7.6. Considerente de design a claselor

În această secţiune se vor prezenta câteva considerente care trebuiesc urmate la alegerea

între utilizarea compoziţiei şi a moştenirii la crearea aplicaţiilor orientate obiect.

Compoziţia şi moştenirea reprezintă două modalităţi prin care programele existente se pot

extinde şi reutiliza. Avantajul principal al acestor două modalităţi de reutilizare a codului

este posibilitatea de dezvoltare incrementală a programelor. Astfel, putem scrie cod nou,

respective să adăugăm facilităţi noi programelor existente fără să fie nevoie să rescriem

codul deja existent. Astfel evităm introducerea de buguri în programele existente care

probabil sunt deja funcţionale. Bugurile care apar în programe pot fi astfel izolate la

nivelul noului cod scris.

Page 38: Silabus Limbaje si medii de programare

Este important de înţeles că dezvoltarea incrementală a programelor este similară cu

procesul uman de învăţare. Astfel, referitor la un domeniu anume, putem realiza o

activitate de analiză oricât de cuprinzătoare dar nu vom reuşi să cuprindem toate

aspectele domeniului la momentul realizării analizei. Astfel, pentru a putea continua şi a

reuşi să furnizăm software util, dezvoltarea incrementală devine esenţială.

2.7.7. Subtyping

În general, compoziţia se utilizează atunci când dorim să utilizăm caracteristicile unui

obiect dintr-o clasă existentă, dar nu dorim să furnizăm aceeaşi interfaţă. Astfel, vom

agrega obiectul din clasa existentă ca şi un membru privat al noii clase.

O a doua modalitate de utilizare a agregării este atunci când dorim să extindem un tip

existent cu o nouă funcţionalitate. Dacă pentru realizarea acestei noi funcţionalităţi avem

nevoie de date noi, o soluţie ar fi agregarea unui obiect din tipul existent împreună cu

datele noi şi crearea unui tip nou. Tipul nou trebuie să fie echipat cu un mecanism de

conversie automată înspre tipul care se doreşte extins, pentru a putea utiliza noile obiecte

cu funcţiile client existente deja pentru tipul vechi.

Această abordare a problemei este depăşită, datorită faptului că ea solicită rescrierea

tuturor metodelor interfeţei tipului vechi în noul tip. Chiar dacă am echipat noua clasă cu

un mecanism de conversie automată, aceasta nu se realizează în cazul în care obiectul din

tipul respectiv este prezent într-o expresie ca şi operand stânga. În acest caz, soluţia este

utilizarea moştenirii.

De exemplu, putem extinde clasa ifstream, adăugând o nouă funcţionalitate: memorarea

numelui fişierului asociat obiectului de tip ifstream. Pentru aceasta, noua clasă trebuie să

moştenească din clasa ifstream şi să agrege un obiect de tipul string în care să se

memoreze numele fişierului asociat. Acest procedeu se numeşte sub-typing.

2.8. Upcast

Upcast reprezintă una din proprietăţile cele mai importante introduse de paradigma

obiectuală prin conceptual de moştenire. Astfel, upcast înseamnă că obiectele din tipul

claselor derivate sunt în acelasi timp şi din tipul clasei de bază.

Să considerăm o clasă de bază Intrument, şi o clasă derivată Wind. Clasa derivată

moşteneşte toate metodele interfeţei clasei de bază. Astfel, dacă vom putea trimite un

mesaj unui obiect de tip Instrument, acelaşi mesaj îl vom putem trimite şi unui obiect

de tipul Wind. Astfel, putem spune că un obiect de tip Wind este în acelaşi timp şi de

Page 39: Silabus Limbaje si medii de programare

tipul Instrument. Clasa Instrument deţine o metodă play care este specifică claselor

derivate, precum clasa Wind.

Oricum, la transmiterea argumentelor pentru o funcţia redefinită a clasei Instrument,

compilatorul realizează conversia automată a pointerului de tip Wind în pointer de tip

Instrument. Aceasta reprezintă în fapt, operaţia de upcast.

Revenind la întrebarea referitoare la alegerea între compoziţie şi moştenire, una din

modalităţile cele mai bune de a decide care concept să-l folosim este să încercăm să

răspundem la următoarea întrebare: pe parcursul programului, operaţia de upcast va fi

necesară? În caz afirmativ, se recomandă utilizarea moştenirii. În caz negativ, utilizarea

compoziţiei.

2.8.1. Extensibilitate prin func ţii virtuale

La apelul unei funcţii, aducerea în contextul curent şi execuţia corpului unei funcţii se

numeşte legarea funcţiilor . Atunci când legarea funcţiilor se face înainte de execuţia

programului, se spune că avem legare “înainte” (early binding). Early binding se face

întotdeauna în faza de link-editare a programului. În limbajele procedurale (C) early

binding reprezintă singura modalitate de legare a corpului unei funcţii de apelul acesteia.

Alternativa la early binding o reprezintă “legarea întârziată” ( late binding) adică legarea

dinamică a corpului funcţiei de apelul acesteia exact înainte de realizarea apelului.

Legarea întârziată presupune existenţa unui mecanism care să determine tipul exact al

obiectului înainte de apelul funcţiei. Astfel, prin determinarea exactă a tipului înainte de

apel, se poate face legare la versiunea corectă a funcţiei, şi anume la cea re-definită în

cazul în care ne situăm într-un program cu ierarhii de clase.

Pentru a determina activarea mecanismului de legare întârziată pentru o funcţie declaraţia

acestei funcţii în clasa de bază trebuie precedată de cuvântul cheie virtual. Redefinirea

unei funcţii virtuale într-o clasă derivată se numeşte “overriding”. Prin declararea virtuală

a unei funcţii într-o clasă de bază, pentru oricare din funcţiile redefinite în clasele

derivate se va realiza legarea întârziată.

In exemplul analizat, aceasta presupune inserarea cuvântului cheie virtual înainte de

declaraţia metodei play în clasa Instrument.

Mecanismul funcţiilor virtuale funcţionează indiferent de nivelul de moştenire.

În contextul polimorfismului, metodele care se apelează pentru obiecte din clasele de

bază (precum metoda tune) pot fi privite ca şi o trimitere de mesaj către un obiect şi

responsabilizarea obiectului destinaţie cu privire la modul de tratare a mesajului. Astfel,

Page 40: Silabus Limbaje si medii de programare

obiectul (în funcţie de tipul său determinat la execuţie) va trata mesajul într-o manieră

proprie, specifică.

2.8.2. Mecanismul de implementare a funcţiilor virtuale

Mecanismul funcţiilor virtuale se bazează pe o tabelă numită tabela funcţiilor virtuale.

Această tabelă există pentru fiecare clasă care conţine funcţii virtuale şi este gestionată

de compilator. Astfel, atunci când compilatorul întâlneşte la compilarea unei clase

declaraţia unei funcţii virtuale, plasează în această tabelă un pointer către această funcţie.

Astfel, tabela funcţiilor virtuale va conţine referinţe către toate funcţiile virtuale, pentru o

anume clasă. În fiecare clasă care conţine funcţii virtuale, compilatorul va crea un

membru (ascuns) care este o referinţă către tabela funcţiilor virtuale. La apelul unei

funcţii virtuale printr-o referinţă de tip upcast, compilatorul generează cod (în codul

obiect rezultat după compilare) care se va executa exact înainte de realizarea apelului.

Acest cod accesează tabela funcţiilor virtuale prin pointerul membru ascuns şi datorită

faptului că la execuţie se cunoaşte tipul obiectului sub care se face apelul, se regăseşte din

tabela funcţiilor virtuale adresa corectă a funcţiei care trebuie să se apeleze şi astfel se

asigură apelul funcţiei corespunzătoare.

Mecanismul descris mai sus este realizat în mod automat de compilator. Singura

problemă rămasă neclarificată este modul în care se realizează determinarea tipului la

execuţie. Aceasta este necesară pentru lucrul cu tabela funcţiilor virtuale.

Modul de memorare a tipului în cadrul obiectelor se poate determina prin crearea de

obiecte fără date membre. Astfel, chiar dacă avem un tip care nu are date membre, la

crearea de obiecte din tipul respectiv vom vedea că obiectele respective ocupă memorie.

Deci sizeof aplicat pe acele obiecte returnează o valoare diferită de 0. Zona respectivă

de memorie este ocupată de un membru “dummy” care există in orice tip şi deci în orice

obiect. Acel membru dummy este utilizat tocmai pentru a stoca tipul obiectului.

Prin interogarea, la execuţie, a membrului dummy se poate afla tipul concret al obiectului

(la execuţie).

Operaţia de upcast se realizează doar în cazul convertirii între tipuri-adrese. Astfel, având

adrese la dispoziţie, şi nu obiecte efective, compilatorul poate amâna realizarea legării

unui apel de funcţie cu corpul funcţiei.

Regula de bază după care funcţionează compilatorul este că ori de câte ori are

posibilitatea, acesta realizează legare “înainte”. Astfel, în cazul obiectelor efective

(variabilelor de tip obiect), întotdeauna se utilizează early binding, iar pentru utilizarea

mecanismului funcţiilor virtuale e nevoie de folosirea pointerilor.

Page 41: Silabus Limbaje si medii de programare

Mecanismul funcţiilor virtuale asigură apelul funcţiei potrivite la contextul potrivit. Se

pune întrebarea de ce acest mecanism nu este apelat întotdeauna, pentru orice apel de

funcţie?

Problema este una de eficienţă. Astfel, un apel de funcţie virtuală este destul de costisitor

- ca şi timp de execuţie deoarece presupune inserare de cod suplimentar la momentul

compilării, şi

- ca şi spaţiu de memorie ocupată deoarece e nevoie de o tabelă suplimentară, tabela

funcţiilor virtuale.

Datorită faptului ca C++ provine din C care este un limbaj orientat spre eficienţă,

alegerea a fost să se ofere mecanismul funcţiilor virtuale doar la cerere. Deci, implicit o

funcţie în C++ este non-virtuală. Se recomandă utilizarea funcţiilor virtuale doar în cazul

creării de clase polimorfice, deci a utilizării conceptului de moştenire.

2.9. Clase abstracte

De multe ori în designul unei aplicaţii dorim să punem în clasa de bază doar interfaţa

viitoarelor obiecte, şi nu să creăm obiecte din clasa respectivă. Clasa de bază va fi

folosită doar pentru operaţia de upcast care va permite astfel tratarea unitară a obiectelor

derivate, care au aceeaşi interfaţă. Pentru a realiza acest lucru, vom transforma clasa de

bază într-o clasă abstractă.

O clasă se numeşte abstractă dacă are cel puţin o funcţie virtuală pură. O funcţie virtuală

pură este întotdeauna precedată de cuvântul cheie virtual şi declaraţia e urmată de =0.

Compilatorul împiedică definirea de obiecte din clasele abstracte.

Atunci când se moşteneşte dintr-o clasă abstractă, clasa derivată trebuie să implementeze

toate funcţiile virtuale pure. În cazul în care în clasa derivată nu este implementată cel

puţin o funcţie virtuală pură, clasa derivată devine la rândul său o clasă abstractă.

Funcţiile virtuale pure forţează astfel să fie implementate, în clasele derivate, care se

doresc a fi clase efective.

În exemplul cu ierarhia de instrumente muzicale, metodele din interfaţa acestei clase sunt

doar de complezenţă, adică au fost definite deoarece este nevoie de definirea lor pentru ca

programul să funcţioneze. Aceste metode nu furnizează un răspuns util. Intenţia clasei

Instrument este să se creeze o interfaţă pentru clasele care se vor deriva, astfel încât

obiectele din clasele derivate să poată fi folosite in mod unitar. Astfel, clasa Instrument

este un bun exemplu de posibilă clasă abstractă. De obicei, se creează o clasă abstractă

atunci când dorim să manipulăm obiecte din diverse clase printr-o interfaţă comună.

Clasele abstracte de obicei nu implementează interfaţa respectivă.

Page 42: Silabus Limbaje si medii de programare

În alte limbaje obiectuale, există conceptual distinct de interfaţă. Astfel, în Java o

interfaţă este similară unei clase abstracte, cu deosebirea că interfeţele nu agregă obiecte,

şi toate metodele din interfaţă sunt metode virtuale pure.

Prin utilizarea funcţiilor virtuale pure şi a claselor abstracte se împiedică crearea efectivă

de obiecte din aceste clase. Astfel, tipul unei clase abstracte nu poate fi folosit ca şi tip de

argument transmis prin valoare, în cazul unei funcţii oarecare. Se forţează astfel utilizarea

pointerilor, deci asocierea dintre upcast şi pointeri.

Cu toate că de obicei funcţiile virtuale pure nu sunt implementate în clasa abstractă, este

totuşi posibil să se scrie implementarea acestor funcţii. Chiar dacă o clasă abstractă va

implementa o funcţie virtuală pură (adică funcţia este declarată în definiţia clasei ca şi

virtuală pură, dar programatorul scrie o definiţie pentru această funcţie), mecanismul

clasei abstracte se păstrează, adică, în continuare, compilatorul împiedică crearea de

obiecte din această clasă.

De multe ori definirea funcţiilor virtuale pure este convenabilă atunci când dorim să

scriem o bucată de cod care să fie apoi apelată din toate sau din unele clase derivate,

eventual din metodele care redefinesc această funcţie.

2.9.1. Implementarea claselor în contextul funcţiilor virtuale

În cazul moştenirii şi a redefinirii, se pune problema modului de funcţionare a funcţiilor

virtuale. Să presupunem cazul în care în clasa de bază există funcţii virtuale. În acest caz,

se creează o tabelă a funcţiilor virtuale pentru clasa de bază, tabelă care conţine locaţii

pentru toate funcţiile virtuale declarate în clasă. În cazul în care se creează o nouă clasă

derivată din clasa de bază, tabela funcţiilor virtuale pentru noua clasă va conţine intrări

pentru funcţiile virtuale din clasa de bază exact la aceeaşi indici ca şi tabela funcţiilor

virtuale a clasei de bază. Dacă redefinim o funcţie virtuală a clasei de bază, tabela

funcţiilor virtuale pentru clasa derivată va memora adresa funcţiei redefinite în locul

adresei funcţiei din clasa de bază. Dacă clasa derivată declară are în plus alte funcţii

virtuale, acestea se vor adăuga în locaţii succesive în tabela funcţiilor virtuale din clasa

derivată. Astfel, modul de scriere (declarare) a funcţiilor virtuale nu este identic cu modul

de stocare a acestora în tabela funcţiilor virtuale.

Astfel, la realizarea operaţiei de upcast, atunci când compilatorul vede o referinţă la un

obiect din clasa de bază, el de fapt vede (şi accesează) zona de tabelă virtuală

corespunzătoare obiectului din clasa derivată (la momentul apelului). La utilizarea unui

pointer dintr-un tip de bază, chiar dacă acesta referă un obiect din clasa derivată,

compilatorul nu va şti să acceseze decât funcţii din propria tabela virtuală. Pentru a se

Page 43: Silabus Limbaje si medii de programare

accesa şi alte funcţii, e nevoie de realizarea explicită a operaţiei de “downcast” (conversie

explicită înspre tipul clasei derivate). La realizarea operaţiei de downcast, programatorul

trebuie să ştie ca întradevăr, la momentul apelului, pointerul respectiv referă un obiect din

tipul derivat. Compilatorul nu are de unde să cunoască tipul obiectului la momentul

realizării apelului.

Transmiterea prin referinţă permite folosirea upcast-ului. Acesta este posibil datorită

faptului că, indiferent de tipul actual referit, pointerii au aceeaşi mărime fizică. În cazul

obiectelor, dacă ele sunt din tipuri diferite este de aşteptat ca ele să ocupe spaţii de

memorie de mărime diferită. Mai mult, în cazul derivării, un obiect din clasa derivată

ocupă cel puţin atâta memorie cât obiectul din clasa de bază. Astfel, dacă transmitem prin

valoare un obiect din clasa derivată într-un obiect din clasa de bază se realizează o

transferare efectivă doar a sub-obiectului din clasa de bază, conţinut în obiectul din clasa

derivată. Deci, tot ce s-a redefinit sau adăugat prin derivare se pierde. Compilatorul nu

va genera eroare deoarece tipurile sunt compatibile.

Suprascrierea unei funcţii definite în clasa de bază ascunde toate definiţiile şi formele

funcţiei respective din clasa de bază. Această regulă se menţine şi în cazul în care se

utilizează funcţiile virtuale, cu o singură excepţie: nu vom putea redefini o funcţie

virtuală prin schimbarea tipului de return. Mai mult, dacă redefinim o funcţie virtuală şi

schimbăm semnătura funcţiei (numărul şi tipul argumentelor) atunci în clasa derivată se

va vadea numai noua funcţie. În schimb, când realizăm operaţia de upcast se vor vedea

numai funcţiile din clasa de bază.

Astfel prin redefinire putem schimba tipul de return a funcţiei din clasa de bază, dacă

funcţia respectivă nu este virtuală. Această regulă e îngrădită puţin în cazul în care

funcţia din clasa de bază returnează un pointer către un tip care la rândul său poate fi

vârful unei ierarhii de clase. Astfel, în clasa derivată, dacă dorim să schimbăm tipul de

return, putem cel mult să îl schimbăm într-un pointer către o clasă derivată din tipul de

return a clasei de bază.

În cazul claselor care conţin funcţii virtuale, constructorii acestor clase realizează

iniţializarea tabelei funcţiilor virtuale. Această operaţie e realizată în mod automat de

constructor. Astfel, compilatorul plasează cod la intrarea în orice constructor care să

iniţializeze tabela funcţiilor virtuale.

La apelul constructorilor, ordinea de iniţializare se păstrează. Astfel, la construirea unui

obiect dintr-o clasă derivată, mai întâi se apelează constructorul clasei de bază şi apoi se

trece la execuţia constructorului curent. Apelul constructorului clasei de bază se

realizează la intrarea în constructorul clasei curente.

Page 44: Silabus Limbaje si medii de programare

În cazul în care din constructor se apelează o funcţie virtuală, se va face legare doar către

versiunea locală (din clasa) a acelei funcţii. În cadrul constructorului mecanismul

funcţiilor virtuale e dezactivat. O funcţionalitate similară se întâmplă şi în cazul

destructorilor. Astfel, dacă într-un destructor se apelează o funcţie virtuală, întotdeauna

de va apela versiunea locală a acestei funcţii.

În cazul constructorilor nu se poate folosi cuvântul cheie virtual.

În cazul destructorilor se poate folosi cuvântul cheie virtual şi astfel se creează destructori

virtuali.

Apelul destructorilor se realizează în ordine inversă faţă cel al constructorilor. Astfel,

destructorii se apelează pornind de la clasa cea mai din josul ierarhiei şi continuă până la

apelul destructorului clasei din vârful ierarhiei.

Folosirea destructorilor virtuali intervine atunci când dorim să eliberăm memoria referită

de pointeri generici, care indică către obiecte din tipul de bază. Astfel de pointeri, pot

referi (prin upcast) obiecte derivate. Dacă apelăm operatorul delete pentru un asemenea

pointer, el va şti să apeleze destructorul clasei de bază, care nu are cum să elibereze

corect memoria pentru un obiect din clasa derivată. In acest caz, e nevoie ca obiectele din

clasa derivată să conţină destructorul în lista funcţiilor virtuale, pentru a se putea apela

destructorul corect.

2.9.2. Downcast

Downcast reprezintă operaţia de conversie între un tip din partea de sus a unei ierarhii şi

un tip specializat. Downcast este operaţia inversă upcastului. La upcast realizarea

conversiei este o chestiune simplă, datorită faptului că se cunoaşte şi se determină exact

tipul destinaţie a conversiei. La downcast, problema apare datorită faptului că tipul

destinaţie nu este unic determinat.

2.10. Design pattern-i

Prin pattern ne referim la o problemă (de programare) care se repetă foarte frecvent în

proiectele pe care trebuie să le realizăm. Datorită acestei frecvenţe a repetării, putem să

presupunem că la fiecare întâlnire a problemei respective vom folosi acelaşi procedeu

rezolvare. Deci, prin pattern putem înţelege atât problema în sine cât şi soluţia adoptată

generic pentru rezolvarea acestei probleme. Astfel, un design pattern reprezintă o soluţie

conceptuală în termeni de programare obiectuală furnizată unui tip probleme specifice.

Page 45: Silabus Limbaje si medii de programare

Design pattern-urile reprezintă un nivel superior de înţelegere a programării obiectuale şi

ajută programatorii să realizeze soluţii scalabile, flexibile la modificarea cerinţelor, re-

utilizabile în condiţiile unor probleme similare.

2.10.1. Singleton

Singleton reprezintă unul din cele mai folosite şabloane de proiectare (design pattern).

Singleton-ul se utilizează atunci când dorim să ne asigurăm că doar un singur obiect

dintr-o clasă va putea exista la un moment dat în aplicaţie. Un singleton se defineşte prin

agregarea ca şi dată membru statică a unei clase a unui obiect de tipul clasei respective.

Prin declararea constructorului clasei respective ca şi privat se poate obţine existenţa unui

singur obiect din clasa respectivă, pe toată execuţia programului. Accesarea acestui

obiect se realizează prin intermediul unei funcţii statice. Astfel, singleton-ul asigură un

punct de acces global la obiectul respectiv.

Figura 19.5 prezintă diagramă UML asociată pattern-ului Singleton. Figura 45 prezintă

implementarea pattern-ului în C++.

+static getInstance() : *Singleton-Singleton()

-static instance : Singleton

Singleton

1

1

Figura 19.5 Design pattern-ul Singleton în specificaţie UML

Declararea privată a unui constructor de copiere este necesară pentru a înlătura alte

modalităţi de creare a obiectelor de tip Singleton. Constructorii de copiere vor fi trataţi în

capitolul următor.

2.10.2. Iterator

Un iterator este un obiect asociat unei colecţii de obiecte. Prin intermediul iteratorului

putem avea acces la câte un membru al colecţiei la un moment dat. Prin intermediul

iteratorului se inhibă accesul direct la colecţia asociată acestuia. Supraîncărcarea

operatorului de dereferenţiere se realizează printr-o funcţie membră. Aceasta trebuie să

returneze un obiect (sau o referinţă către un obiect) care poate fi operand în operaţia de

dereferenţiere (sau poate returna un pointer care să permită operaţia de selecţie).

Page 46: Silabus Limbaje si medii de programare

+f() : void+g() : void

-static i : int-static j : int

Obj

+add(in obj : Obj*) : void

-a : vector<Obj*>

ObjContainer

+SmartPointer(in objc : ObjContainer&)+operator++() : bool+operator++(in dummy : int) : bool+operator ->() : Obj*

-oc : ObjContainer&

SmartPointer

Figura 19.6 Diagrama UML asociată unui iterator implementat prin suprascrierea operatorilor

2.11. Întreb ări recapitulative

1. Care sunt principalele caracteristici ale unui limbaj de programare obiectual?

2. Descrieţi actorii care participa la scrierea şi utilizarea programelor informatice. Care

este scopul acestor actori?

3. Care sunt paşii care trebuie urmaţi pentru realizarea programelor, conform

metodologiei obiectuale?

4. Descrieţi metoda pentru eXtreme Programming, pentru realizarea programelor

informatice.

5. Ce este un obiect obiect? În ce constă implementarea şi interfaţa obiectului?

6. Ce înţelegeţi prin „separarea interfeţei de implementare” şi cum se realizează acest

lucru în C++?

7. De ce este necesară ascunderea implementării?

8. Care sunt specificatorii de acces folosiţi în C++?

9. Ce este un constructor? Care sunt tipurile de constructori posibili ai unei clase?

10. Ce este un destructor?

11. Ce este un constructor de copiere? De ce este acesta important?

12. Cum se poate evita folosirea constructorului de copiere în programele obiectuale?

13. Ce înţelegeţi prin „supraîncărcarea funcţiilor”?

14. Ce înţelegeţi prin „supraîncărcarea operatorilor”?

15. Enunţaţi câteva reguli de care trebuie să se ţină cont la supraîncărcarea operatorilor.

16. Care sunt principiile care trebuie aplicate la supraîncărcarea operatorului de atribuire?

17. Care este criteriul esenţial care trebuie aplicat la alegerea între supraîncărcarea unui

operator prin funcţie membră sau funcţie globală?

Page 47: Silabus Limbaje si medii de programare

18. Descrieţi metoda numită „numărarea referinţelor” care se poate aplica la

supraîncărcarea operatorului de atribuire.

19. Care sunt acţiunile care se efectuează la crearea efectivă unui obiect pe parcursul

rulării programului?

20. Cum se realizează alocarea memoriei în C++?

21. Cum se realizează crearea dinamică a obiectelor în C++?

22. Descrieţi alternativele existente de gestiune a memoriei în C++?

23. Cum realizează limbajul de programare JAVA gestiunea memoriei?

24. Definiţi compoziţia. De câte tipuri este aceasta?

25. Definiţi moştenirea şi enuntaţi principalele caracteristici ale acesteia.

26. Cum se realizează iniţializarea obiectelor în cazul combinării compoziţiei cu

moştenirea?

27. Cum se realizează ascunderea numelor prin moştenire? Ce este specialiarea?

28. Ce înseamnă subtyping? Cum se poate realiza acest procedeu prin compoziţie

respectiv prin moştenire?

29. Ce înseamnă upcast?

30. Definiţi conceptul de legare a funcţiilor şi cum se realizează acest concept?

31. Ce este o funcţie virtuală? Care sunt avantajele folosirii acestor funcţii?

32. Care este mecanismul de implementare a funcţiilor virtuale?

33. Ce înseamnă „determinarea tipului la execuţie”?

34. Ce este o clasă abstractă?

35. Definiţi conceptul de interfaţă în limbajele obiectuale.

36. Care sunt diferenţele între supraîncărcarea funcţiilor, refedinirea funcţiilor în

contextul moştenirii şi „overriding”?

37. Ce este un design pattern?

38. Descrieţi design patternul „singleton”.

39. Descrieţi design patternul „iterator”.

Page 48: Silabus Limbaje si medii de programare

III. Anexe

Bibliografie

1. G.C. Silaghi, Mircea Moca „Limbaje de Programare. Metode Obiectuale. Ghid teoretic

si practic”. Editia 2-a, Ed. Risoprint, 2008

2. Bruce Eckel, „Thinking in C++”, ed. Prentice Hall, 2000, vol 1.

3. Bjarne Stroustrup, „The C++ Programming Language”, Addison-Wesley,

4. Bazil Pârv, Al. Vancea, „Fundamentele limbajelor de programare”, 1996

5. D.L. Şerbănaţi, „Limbaje de programare şi compilatoare”, Ed. Academiei, 1987

6. Malcom Bull, „Student’s guide to programming languages”, HB Newnes, 1992

7. Aho A.V., Ullman J.D, Principles of Compiler Design, Addison-Wesley, 1977

Scurtă biografie a titularului de curs

Gheorghe Cosmin Silaghi este absolvent al secţiei Informatică Economică a Universităţii

Babeş-Bolyai Cluj-Napoca din anul 2000 si doctor in Stiinţe Economice – Cibernetică şi

Statistică Economică din anul 2005. Este lector universitar din anul 2006. Gheorghe

Cosmin Silaghi are o progătire vastă in domeniul informaticii fiind inginer in Stiinţa

Sistemelor si Calculatoarelor, absolvind programul de master of Science in Inteligenţă

Artificial ă a Universităţii Libere din Amsterdam. Pregătirea postdoctorală a titlularului de

curs include o participare de 18 luni in proiectul european FP6 CoreGrid fiind detaşat la

Rutherford Applenton Laboratory Anglia (2006-2007) şi Universitatea din Coimbra

Portugalia (2007). Activitatea de cercetare este completată de o serie de publicaţii

relevante realizate şi proiecte de cercetare conduse.

G.C. Silaghi este titularul acestui curs incepând cu anul 2005, după ce a realizat şi condus

laboratoarele la această disciplină incepând cu anul 2002. Experienţa de predare necesară

disciplinei este completată de experienţă practică efectivă, titularul de curs activând pe

rând ca şi programator, analist business şi consultant la diverse companii software.