poo - curs doc

112
Programare orientată pe obiecte (suport de curs) §1. Principiile programării orientate pe obiecte. Strategia procedurală şi structurată de scriere a programelor presupune existenţa unui program principal şi a unui şir de proceduri (subprograme, funcţii), iar rezultatul final este obţinut prin apelarea procedurilor respective transmiţându-le lor datele necesare: Programarea orientată pe obiecte (POO) este tehnologia de programare în care programul este reprezentat ca o colecţie de obiecte discrete, care conţin nişte seturi de structuri de date şi proceduri ce interacţionează cu alte obiecte: Există mai multe sensuri ale cuvântului „obiect”: corp solid, cunoscut direct cu ajutorul simţului; lucru sau complex de lucruri apărute ca rezultat al procesului de muncă (de exemplu, obiect de consum, obiect de uz casnic); materie asupra căreia este orientată activitatea spirituală sau artistică (de exemplu, obiect de cercetare, obiect de descriere); fiinţă sau lucru pentru care cineva manifestă un sentiment (de exemplu, obiect de admiraţie); 1

Upload: tyka-surdu

Post on 04-Feb-2016

76 views

Category:

Documents


2 download

DESCRIPTION

POO Curs FREE..

TRANSCRIPT

Page 1: POO - Curs Doc

Programare orientată pe obiecte(suport de curs)

§1. Principiile programării orientate pe obiecte.

Strategia procedurală şi structurată de scriere a programelor presupune existenţa unui program principal şi a unui şir de proceduri (subprograme, funcţii), iar rezultatul final este obţinut prin apelarea procedurilor respective transmiţându-le lor datele necesare:

Programarea orientată pe obiecte (POO) este tehnologia de programare în care programul este reprezentat ca o colecţie de obiecte discrete, care conţin nişte seturi de structuri de date şi proceduri ce interacţionează cu alte obiecte:

Există mai multe sensuri ale cuvântului „obiect”:

corp solid, cunoscut direct cu ajutorul simţului;

lucru sau complex de lucruri apărute ca rezultat al procesului de muncă (de exemplu, obiect de consum, obiect de uz casnic);

materie asupra căreia este orientată activitatea spirituală sau artistică (de exemplu, obiect de cercetare, obiect de descriere);

fiinţă sau lucru pentru care cineva manifestă un sentiment (de exemplu, obiect de admiraţie);

disciplină de studiu într-o instituţie de învăţământ;

filos. Corp sau fenomen existent în realitate, în afara subiectului şi independent de conştiinţa acestuia;

lingv. Parte de propoziţie care indică asupra cui este orientată acţiunea verbului; complement.

1

Page 2: POO - Curs Doc

Aceste definiţii cuprind diferite domenii ce ţin de natură şi de activitatea omului. Ele toate pot fi modelate în POO prin declararea claselor şi crearea obiectelor respective.

Pentru a aplica tehnologia orientată pe obiecte, iniţial, este necesar de a realiza o etapă importantă - abstractizarea. Modelul fizic al oricărui domeniu de problemă este unul complex, depinzând de mulţi parametri şi relaţii. Astfel, se face o simplificare a modelului fizic, obţinând modelul logic al domeniului de problemă. Sunt omise din modelul fizic o serie de proprietăţi şi legături care nu sunt esenţiale în contextul problemei formulate. Acest proces de simplificare se numeşte abstractizare:

Deci, abstractizarea presupune „sărăcirea” datelor iniţiale.

Exemplu. La elaborarea unui program de evidenţă a cadrelor într-o întreprindere, este necesar să descriem obiectul „Angajat al întreprinderii” care în esenţă este o persoană (o fiinţă umană). Orice persoană angajată la întreprindere poate fi descrisă (caracterizată) printr-un set de proprietăţi, cum ar fi de exemplu:

- nume;- prenume;- domiciliu;- anul naşterii;- funcţia;- salariu;- vechimea în muncă;

- înălţime;- culoarea ochilor;- culoarea părului;- greutatea;- numărul de copii;- etc.

De aici se observă că o serie de proprietăţi nu sunt adecvate problemei formulate, de exemplu, înălţime, culoarea ochilor, culoarea părului şi greutatea, ele fiind eliminate din descriere.

Pe lângă proprietăţi un obiect poate fi caracterizat şi printr-o serie de operaţii (funcţionalitate), de exemplu:

- Schimbarea salariului;- Schimbarea domiciliului;- Schimbarea funcţiei;- Creşterea vechimii în muncă;- Schimbarea numelui;- etc.

Definiţie. Obiectul în programare reprezintă modulul de program care îndeplineşte următoarele cerinţe principale:

- întruneşte date (caracterizează proprietăţile obiectului) şi operaţiile asupra lor (se mai numesc metode şi caracterizează comportarea obiectului sau posibilităţile obiectului, adică ceea ce poate face obiectul);

- posedă proprietăţile de moştenire, încapsulare şi polimorfism.

La baza programării orientate pe obiecte stă noţiunea de clasă.

O clasă în programare întruneşte toate obiectele de una şi aceeaşi natură (la fel ca şi în lumea reală). Obiectele care aparţin uneia şi aceleiaşi clase posedă una şi aceeaşi structură, comportare şi relaţii cu obiecte din alte clase.

abstractizare

Modelul logic

Modelul fizic

2

Page 3: POO - Curs Doc

Definiţie. Clasa este tipul abstract de date creat de utilizator (programator). În clasă se descriu împreună câmpurile de date şi metodele sub formă de funcţii membre. Metodele efectuează operaţii asupra acestor date şi alte acţiuni ce caracterizează comportamentul obiectelor clasei respective. În baza claselor deja create există posibilitatea de a crea clase derivate care moştenesc proprietăţile claselor de bază.

Exemplarul de obiect în POO se mai numeşte instanţă (engl. instance), este un obiect concret din setul de obiecte ale uneia şi aceleiaşi clase. Crearea exemplarului de obiect al unei clase se numeşte instanţierea obiectului.

Când creăm o clasă, definim implicit un nou tip de date. Deci, un obiect este o variabilă de un tip definit de utilizator (tipul clasă).

Programarea orientată pe obiecte se bazează pe un şir de principii.

Principiul 1. Încapsulare.Încapsularea în POO (engl. encapsulation) este unirea într-un singur tot a datelor şi metodelor, ceea

ce permite ascunderea structurii interne de date şi a metodelor obiectului de restul programului. Celelalte obiecte din program au acces numai la interfaţa obiectului. Interfaţa este reprezentată prin membrii publici prin care se efectuează toate interacţiunile cu acest obiect.

Încapsularea este un mecanism care leagă într-un tot întreg codul şi datele, păstrându-le pe ambele în siguranţă contra intervenţiilor din afară şi de utilizări greşite. În plus, încapsularea asigură crearea obiectelor. Un obiect este o entitate logică ce încapsulează atât date, cât şi codul care manevrează aceste date. Într-un obiect, o parte din cod şi/sau date pot fi particulare (private sau protejate) acelui obiect şi inaccesibile pentru oricine din afara lui. Astfel, un obiect dispune de un nivel semnificativ de protecţie care împiedică modificarea accidentală sau utilizarea incorectă a părţilor obiectului de către secţiuni ale programului cu care acestea nu au legătură.

Principiul 2. Moştenire.Moştenirea în POO (engl. inheritance) reprezintă proprietatea obiectului, care constă în aceea că

caracteristicile unui obiect (obiectul-părinte) pot fi transmise unui alt obiect (obiect-fiu) fără descrierea lor repetată. Moştenirea simplifică descrierea obiectelor şi admite clasificarea lor.

Exemplu. Clasa furnici face parte din clasa de insecte fără aripi, care la rândul său face parte din clasa mai generală insecte. O astfel de clasificare se numeşte taxonomie. Taxonomia este o ştiinţă care se ocupă cu stabilirea legilor de clasificare şi de sistematizare a domeniilor cu o structură complexă.

Fără utilizarea claselor cu proprietatea de moştenire fiecare obiect ar trebui definit cu enumerarea tuturor caracteristicilor sale. Însă, prin folosirea clasificărilor, un obiect are nevoie doar de definirea

Greieri Furnici Muşte Fluturi

Insecte înaripate

Insecte fără aripi

Insecte

Exemplu de ierarhie taxonomică

3

Page 4: POO - Curs Doc

acelor calităţi care îl fac unic în clasa sa. Mecanismul moştenirii permite ca un obiect să fie descris ca un exemplar al unui caz mai general.

Principiul 3. Polimorfism.În limbajele de programare orientate pe obiecte polimorfismul este caracterizat prin fraza „o singură

interfaţă, metode multiple”. Polimorfismul în POO (engl. polymorphism) este capacitatea obiectului de a selecta, din mai multe metode metoda corectă, în dependenţă de tipul de date, primite în mesaj, sau, altfel spus, posibilitatea de a denumi la fel diferite acţiuni la diferite niveluri ale ierarhiei de clase. Polimorfismul simplifică programarea, deoarece selectarea acţiunii necesare se realizează automat.

Dăm un exemplu de polimorfism din matematică. Una şi aceeaşi expresie a+b va fi interpretată în conformitate cu tipul operanzilor operaţiei de adunare. Dacă a şi b sunt două numere, expresia dată reprezintă suma a două numere, dacă a şi b sunt două matrice – rezultatul va fi suma matricelor, dacă a şi b sunt doi vectori – expresia reprezintă vectorul rezultant etc.

În limbajele de programare orientate pe obiecte se folosesc mai multe mecanisme prin care se realizează principiul de polimorfism. Cele mai principale sunt suprascrierea şi supraîncărcarea funcţiilor şi operatorilor, posibilitatea de a specifica valorile implicite pentru parametri, regulile de atribuire între obiecte ale claselor descendente, funcţiile virtuale.

§2. Clase şi obiecte în C++.

În forma cea mai generală o clasă poate fi descrisă astfel:

class [nume_clasa]{//membrii clasei};

unde membrii clasei sunt de două tipuri:- date membre;- funcţii membre.

Cu ajutorul datelor membre pot fi descrise atributele care caracterizează proprietăţile obiectelor reale, modelate în cadrul programelor create. Funcţiile membre descriu funcţionalităţile claselor de obiecte din domeniul de problemă.

Forma generală de descriere a unei clase este următoarea:class [nume_clasa] //identificator{ [specificator_de_acces_a1:] lista_membri_1; specificator_de_acces_a2: lista_membri_2; - - - - - - - - - - -specificator_de_acces_aN:lista_membri_N;} [lista_obiecte] ;

[descrierea_funcţiilor_membre_si_prietene]

unde nume_clasa – un identificator, numele clasei,lista_obiecte – variabile de tip clasă, sau obiecte ale clasei descrise.În această formă de declarare a unei clase, componentele incluse în paranteze pătrate sunt opţionale

(pot fi omise).

4

Page 5: POO - Curs Doc

Componentele din descriere specificator_de_acces_a1, specificator_de_acces_a2, ..., specificator_de_acces_aN, se numesc specificatori de acces şi indică gradul de acces la membrii clasei. Fiecare membru al clasei se află sub acţiunea unui specificator de acces. Specificatorii de acces sunt descrişi prin cuvintele cheie private, protected şi public (rom.: acces privat, protejat, public), urmate de semnul „:”. Următorul tabel descrie gradul de acces permis de fiecare specificator:

Funcţii membre aleclasei date

Funcţii prietene

Funcţii membre ale claselor derivate

Funcţii externe

private + + - -protected + + + -public + + + +

După cum se vede, declaraţia începe cu cuvântul-cheie class, după care urmează denumirea clasei. Denumirea clasei este orice identificator creat de către programator conform sintaxei limbajului C++ şi care este liber în momentul declarării. Apoi, urmează corpul clasei luat în acolade { şi }. Corpul poate fi şi vid. Corpul clasei poate conţine declarări de câmpuri (date) membre, declarări de prototipuri ale funcţiilor membre şi ale funcţiilor prietene, definiri de funcţii membre şi de funcţii prietene, declarări de clase prietene. Declararea câmpurilor şi a funcţiilor membre se repartizează pe secţiuni în dependenţă de regimul dorit de accesare a lor. Acest regim este stabilit prin utilizarea a trei specificatori de acces: public, protected, private. Ordinea secţiunilor, precum şi numărul de repetări sunt arbitrare. Dacă la începutul corpului de declarare a clasei, pentru un set de câmpuri şi funcţii membre specificatorul de acces nu este indicat, implicit, pentru câmpurile şi funcţiile membre din această secţiune (prima secţiune) va fi stabilit regimul de accesare private (privat) – cel mai dur regim de accesare din cele trei. Câmpurile şi funcţiile membre private sunt accesibile numai din interiorul acestei clase, adică acces la ele au numai funcţiile membre ale sale, sau funcţiile de tip prieten (friend) al acestei clase. Regimul de accesare protected (protejat) este puţin mai liber în comparaţie cu regimul private. El stabileşte că câmpurile şi funcţiile membre ale clasei definite vor fi accesibile şi din interiorul claselor derivate ale clasei definite. Specificatorul de acces public este cel mai liber din cei trei. El admite accesarea câmpurilor şi a funcţiilor membre ale acestei secţiuni din orice loc al programului unde va fi vizibil obiectul concret al clasei definite. Aşa o accesare este asigurată prin intermediul numelui acestui obiect.

În C++ declararea claselor este similară cu declararea structurilor. În cazul structurilor se foloseşte cuvântul-cheie struct în loc de class. O deosebire constă în aceea că în cazul structurilor, implicit, va fi stabilit regimul de accesare public şi nu private, cum a fost descris mai sus pentru clase.

Imediat după descrierea corpului clasei (acolada de închidere) putem crea un set de obiecte ale acestei clase. Însă, acest lucru nu este obligatoriu. Obiectele pot fi create şi mai târziu în program.

După caracterul ';' urmează definiţiile (realizările) funcţiilor membre, dacă prototipurile lor au fost declarate în corpul clasei.

Exemplu 1. Declarăm versiunea simplificată a clasei pentru reprezentarea fracţiilor fără semn, cu numărător şi numitor – numere naturale (numere raţionale pozitive). Vom numi această clasă fractie:

#include <conio.h>#include <iostream.h>class fractie{ protected: unsigned int numarat; unsigned int numit;};

5

Page 6: POO - Curs Doc

void main(){ fractie fr1; // creăm un obiect numit fr1, reprezentantul clasei // fractie fractie fr2; // mai creăm un obiect numit fr2, reprezentantul aceleiaşi // clase fr1 = fr2; // atribuim obiectului fr1 obiectul fr2 char c; cin >> c; // se aşteaptă apăsarea oricărei taste}

De fapt, conform sintaxei, o versiune minimală a clasei fractie este:

class fractie{};

Însă, fiind perfect validă din punctul de vedere al sintaxei, ea nu este interesantă din punctul de vedere al semanticii, de aceea nici nu va fi examinată.

Clasa fractie conţine două câmpuri: numarat şi numit pentru păstrarea, respectiv, a numărătorului şi a numitorului ce alcătuiesc o fracţie raţională fără semn. Ambele câmpuri sunt de tipul unsigned int şi au unul şi acelaşi tip de acces protected. Dacă înlăturăm linia:

protected:

din programul de mai sus, atunci, implicit, câmpurile numarat şi numit vor avea tipul de acces private. Însă, în ambele cazuri, cu obiectele create ale clasei fractie putem doar să atribuim un obiect altuia, cum este arătat în funcţia main(). Operatorul de atribuire este implicit realizat cu orice clasă declarată. El presupune copierea datelor (valoarea numărătorului şi numitorului) din a doua fracţie în prima. Nu se ştie ce va fi copiat în fr1 în exemplul nostru, fiindcă nici în câmpurile obiectului fr1, nici în câmpurile obiectului fr2 nu a fost înscrisă nici o valoare până la aplicarea operaţiei de atribuire. Iniţializarea nu a fost prevăzută în versiunea clasei de mai sus. Mai mult ca atât, nici nu există posibilitatea de a înscrie cumva valori în câmpurile obiectelor fr1 şi fr2 (din cauza că sunt protejate). Unicul lucru care poate fi afirmat este că după atribuire, conţinuturile câmpurilor obiectelor fr1 şi fr2 vor fi identice.

Din cauza că câmpurile clasei sunt protejate, orice încercare de accesare, cum ar fi, de exemplu: fr1.numarat = 2; fr2.numit = 3;vor fi respinse de către compilator, fiind interzise (se va semnala o eroare). În versiunea propusă a clasei fractie nu putem iniţializa câmpurile obiectelor create, nu putem

afişa obiectele, nu putem efectua calcule asupra lor etc. Aceste acţiuni se pot realiza cu ajutorul funcţiilor membre (metodelor) ale clasei.

Exemplu 2. De alcătuit un program în care este descrisă clasa carte. În baza acestei clasei sunt create obiecte, care sunt apoi utilizate, apelând la metodele clasei initializare şi afisare.#include<stdio.h>#include<string.h>#include<stdlib.h>class carte{ char* autor;

char* denumire;int an_ed;

public:void initializare (char* a, char* d, int ae);void afisare();void nimicire();

};

6

Page 7: POO - Curs Doc

void carte::initializare(char* a, char* d, int ae){

autor=(char*)malloc(srtlen(a)+1); strcpy(autor,a);

denumire=(char*)malloc(strlen(d)+1);strcpy(denumire,d);

an_ed=ae;}

void carte::afisare(){

printf(”Autorul cartii: %s\n”,autor);printf(”Denumirea cartii: %s\n”,denumire);printf(”Anul editarii: %d\n”,an_ed);

}

void carte::nimicire(){

free (autor);free (denumire);

}

void main (){

class carte em, an;em.initializare(”M.Eminescu”,”Opere complete”,2002);an.initializare(”A.Demidovici”,”Analiza matematica”,1960);

em.afisare();an.afisare();

em.nimicire(); an.numicire();

}

În rezultatul îndeplinirii acestui program se va afişa pe ecran:Autorul cartii: M.EminescuDenumirea cartii: Opere completeAnul editarii: 2002Autorul cartii: A.DemidoviciDenumirea cartii: Analiza matematicaAnul editarii: 1960

§3. Constructori şi destructori.

La crearea oricărui obiect va fi apelată obligatoriu o funcţie membră specială – constructorul clasei. Dacă acest lucru nu va fi făcut direct de către programator, el va fi făcut implicit de către compilator. La distrugerea oricărui obiect va fi apelată obligatoriu o altă funcţie membră specială – destructorul clasei.

Definiţie 1. Constructorul este funcţia membră specială a clasei, destinată pentru iniţializarea fiecărui obiect al acesteia. Iniţializarea se face imediat după crearea obiectului (imediat după declararea variabilei de tip clasă), adică după ce el a fost declarat şi lui i s-a alocat memorie.

Definiţie 2. Destructorul este funcţia membră specială a clasei, destinată pentru efectuarea unor operaţii adăugătoare la distrugerea fiecărui obiect al clasei, când expiră termenul lui de „viaţă”. (De exemplu, eliberarea memoriei alocate obiectului, închiderea sau ştergerea unor fişiere etc. Deci, destructorul are funcţia de a elimina toate „urmele” obiectului la distrugerea lui)

7

Page 8: POO - Curs Doc

În procesul lucrului cu obiectele create ale unor clase, pot apărea situaţii când obiectele nu sunt pregătite pentru operaţiile efectuate. Exemple care ar descrie astfel de situaţii ar putea fi: încercarea de a afişa informaţia despre proprietăţile unui obiect, el ne-fiind iniţializat; operaţii cu membrii ne-iniţializaţi ai unui obiect. Dacă uneori astfel de operaţii generează erori nesemnificative, în unele cazuri urmările por fi mai grave.

Pentru a evita, astfel de situaţii, în cadrul claselor sunt utilizate nişte funcţii membre cu destinaţie specială – constructorii. Constructorul este o funcţie membră care are menirea de a pregăti obiectele create pentru operaţiile ulterioare. De aceea la procesul de creare a oricărui obiect participă şi constructorul clasei.

Exemple de acţiuni puse în responsabilitatea constructorilor pot fi următoarele: - iniţializarea câmpurilor; - alocarea de memorie şi iniţializarea câmpurilor de tip pointer; - deschiderea unor fişiere;- etc.Constructorul unei clase are următoarele particularităţi:- numele constructorului trebuie să fie identic cu numele clasei (obligatoriu);- pentru o clasă se pot defini mai mulţi constructori, fiecare iniţializând obiectele în moduri

diferite. Constructorii trebuie să se deosebească între ei prin numărul de argumente sau tipul argumentelor;

- constructorul nu returnează nici o valoare şi deci, spre deosebire de celelalte funcţii, îi lipseşte din antet sau prototip descrierea tipului valorii returnate;

- constructorul poate fi apelat explicit ca o funcţie membră obişnuită. Dacă nu se apelează nici un constructor la crearea unui obiect, sistemul singur va apela unul din ei, şi anume – constructorul implicit.

Este prezentată următoarea schemă sintactică, care descrie utilizarea constructorilor:class nume_clasa{...nume_clasa([tip1 param1,...,tipN paramN]);...};

//----------------------

nume_clasa :: nume_clasa([tip1 param1,...,tipN paramN]){ //instructiuni}

Fiind o funcţie membră, constructorul poate fi implementat atât în interiorul clasei, cât şi în exteriorul ei.

Există următoarele tipuri de constructori:1) constructorul implicit;2) constructori cu parametri;3) constructorul de copiere;

O clasă poate să nu aibă nici un constructor, poate avea unul sau mai mulţi constructori. Chiar dacă programatorul nu defineşte în interiorul clasei nici un constructor, sistemul va crea automat un constructor – constructorul fără parametri, care se numeşte constructor implicit. Totodată, constructorul implicit poate fi definit şi de către programator.

Tradiţional, constructorul cu parametri atribuie valorile argumentelor sale câmpurilor obiectului care se creează cu acest constructor. Constructori cu parametri pot fi definiţi mai mulţi şi ei, având toţi acelaşi nume, trebuie neapărat să se deosebească între ei prin numărul de argumente sau prin tipul argumentelor.

8

Page 9: POO - Curs Doc

Constructorul de copiere permite construirea unui obiect nou în baza unui obiect existent. Dacă programatorul nu defineşte un constructor de copiere, sistemul va genera un constructor de copiere automat. În urma iniţializării cu ajutorul constructorului de copiere, se obţin două obiecte identice.

O altă categorie de erori posibile în procesul de prelucrare a obiectelor ar putea fi ne-eliberarea de memorie, legată de careva obiect, atunci când obiectul îşi termină existenţa sa, ne-eliberarea altor resurse, ce ţin de obiect. În asemenea situaţii are loc o risipă de resurse, deoarece este pierdut complet controlul asupra lor, odată cu lichidarea obiectului. Astfel de situaţii pot fi ocolite, utilizând nişte funcţii membre cu destinaţie specială, numite destructori. Destructorul este o funcţie membru care are menirea de a efectua o serie de operaţii în contextul distrugerii obiectului. De aceea la procesul de distrugere a oricărui obiect participă şi destructorul clasei. Exemple de acţiuni puse în responsabilitatea destructorilor pot fi următoarele:

- eliberarea memoriei asociate cu obiectul; - închiderea unor fişiere etc.

Destructorul unei clasei are următoarele particularităţi:- numele destructorului este identic cu numele clasei, fiind precedat de simbolul „~”;- similar cu constructorul, destructorul nu returnează nici o valoare;- destructorul nu are parametri;- destructorul nu poate fi apelat explicit după modelul unei funcţii membre obişnuite. Apelarea lui

are loc automat atunci, când obiectul ajunge la sfârşitul domeniului său de scop.

Este prezentată următoarea schemă sintactică, care descrie utilizarea destructorilor:class nume_clasa{...~nume_clasa();...};

//----------------------

nume_clasa :: ~nume_clasa(){ //instructiuni}

Clasa poate şi să nu aibă destructor. În acest caz, este creat un destructor implicit, care de fapt nu efectuează anumite operaţii în procesul de distrugere a obiectului. Clasa poate avea cel mult un destructor definit de programator.

Destructorul se execută în ordine inversă executării constructorului. Deci la sfârşitul lucrului unei funcţii, obiectele create în ea se distrug în ordinea inversă decât cea în care ele au fost create.

Exemplu. Să se definească clasa „Angajat”, obiectele căreia ar conţine informaţia despre angajaţii unei întreprinderi (de exemplu, numele, anul naşterii, funcţia, salariu). Să se definească pentru această clasă trei tipuri de constructori şi destructorul. Să se definească funcţiile membre pentru a) Setarea numelui angajatului, b) Accesarea numelui angajatului, c) Modificarea salariului angajatului, d) Setarea salariului unui angajat ca media aritmetică dintre salariile altor doi angajaţi, e) Afişarea informaţiei despre angajat la ecran.

Programul:

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

9

Page 10: POO - Curs Doc

class Angajat{ // membrii privati private: char* Nume; char* Functia; int An; double Salariu;

// membrii publici public: Angajat(); // constructor implicit Angajat(char* N,char* F,int A,double S); // constructor cu parametri Angajat(char* N,int A); // constructor cu parametri Angajat(double S); // constructor cu parametri Angajat(Angajat& X); // constructor de copiere

~Angajat(); // destructorul

void SetNume(char* N); char* GetNume(); int ModificaSalariu(double S); void SalariuMediu(Angajat& X,Angajat& Y); void Afisare();};

Angajat::Angajat(){ Nume=(char*)malloc(sizeof("Necunoscut")+1); Functia=(char*)malloc(sizeof("-")+1); strcpy(Nume,"Necunoscut"); strcpy(Functia,"-"); An=0; Salariu=0;}

Angajat::Angajat(char* N,char* F,int A,double S){ Nume=(char*)malloc(sizeof(N)+1); Functia=(char*)malloc(sizeof(F)+1); strcpy(Nume,N); strcpy(Functia,F); An=A; Salariu=S;}

Angajat::Angajat(char* N,int A){ Nume=(char*)malloc(sizeof(N)+1); Functia=(char*)malloc(sizeof("-")+1); strcpy(Nume,N); strcpy(Functia,"-"); An=A; Salariu=0;}

Angajat::Angajat(double S){ Nume=(char*)malloc(sizeof("Necunoscut")+1); Functia=(char*)malloc(sizeof("-")+1); strcpy(Nume,"Necunoscut"); strcpy(Functia,"-"); An=0; Salariu=S;}

10

Page 11: POO - Curs Doc

Angajat::Angajat(Angajat& X){ Nume=(char*)malloc(sizeof(X.Nume)+1); Functia=(char*)malloc(sizeof(X.Functia)+1); strcpy(Nume,X.Nume); strcpy(Functia,X.Functia); An=X.An; Salariu=X.Salariu;}

Angajat::~Angajat(){ printf("Obiectul (%s) a fost distrus.\n",Nume); free(Nume); free(Functia);}

void Angajat::SetNume(char* N){ free(Nume); Nume=(char*)malloc(sizeof(N)+1); strcpy(Nume,N);}

char* Angajat::GetNume(){ return Nume;}

// Daca se poate modifica, metoda modifica// si returneaza valoarea 1(Adevar)// Daca nu se poate - nu modifica// si returneaza 0(Fals)int Angajat::ModificaSalariu(double S){ if(Salariu+S < 0) return 0; Salariu=Salariu+S; return 1;}

void Angajat::SalariuMediu(Angajat& X,Angajat& Y){ Salariu=(X.Salariu+Y.Salariu)/2;}

void Angajat::Afisare(){ printf("Nume: %s | Functie: %s | Anul nasterii: %4i | Salariu: %g\n",Nume,Functia,An,Salariu);}

void main(){ clrscr();

// obiect creat cu constructorul implicit Angajat a; a.Afisare(); a.SetNume("Petru Moraru"); a.Afisare(); printf("Numele angajatului (a): %s\n",a.GetNume());

// Urmatoarele doua instructini comentate sunt // interzise, deoarece datele membre sunt private: // printf("%s",a.Nume); // a.Nume is not accesible // a.An=1993; // a.An is not accesible

11

Page 12: POO - Curs Doc

// obiect creat cu 1-ul constructor cu parametri Angajat b("Ion Padure","Manager",1992,1125.75); b.Afisare();

// obiect creat cu al 2-lea constructor cu parametri Angajat c("Dan Balan",1984); c.Afisare();

// obiect creat cu al 3-lea constructor cu parametri Angajat d(756.45); d.Afisare();

// obiect creat cu constructorul de copiere Angajat e(b); e.Afisare();

c.SalariuMediu(b,d); c.Afisare();

getch();}

Rezultat:Nume: Necunoscut | Functie: - | Anul nasterii: 0 | Salariu: 0Nume: Petru Moraru | Functie: - | Anul nasterii: 0 | Salariu: 0Numele angajatului (a): Petru MoraruNume: Ion Padure | Functie: Manager | Anul nasterii: 1992 | Salariu: 1125.75Nume: Dan Balan | Functie: - | Anul nasterii: 1984 | Salariu: 0Nume: Necunoscut | Functie: - | Anul nasterii: 0 | Salariu: 756.45Nume: Ion Padure | Functie: Manager | Anul nasterii: 1992 | Salariu: 1125.75Nume: Dan Balan | Functie: - | Anul nasterii: 1984 | Salariu: 941.1Obiectul (Ion Padure) a fost distrus.Obiectul (Necunoscut) a fost distrus.Obiectul (Dan Balan) a fost distrus.Obiectul (Ion Padure) a fost distrus.Obiectul (Petru Moraru) a fost distrus.

Analiza programului principal main pe blocuri:

1) // obiect creat cu constructorul implicit Angajat a; a.Afisare(); a.SetNume("Petru Moraru"); a.Afisare(); printf("Numele angajatului (a): %s\n",a.GetNume());

Se afişează:Nume: Necunoscut | Functie: - | Anul nasterii: 0 | Salariu: 0Nume: Petru Moraru | Functie: - | Anul nasterii: 0 | Salariu: 0Numele angajatului (a): Petru Moraru

2) // Urmatoarele doua instructini comentate sunt // interzise, deoarece datele membre sunt private: // printf("%s",a.Nume); // a.Nume is not accesible // a.An=1993; // a.An is not accesible

3) // obiect creat cu 1-ul constructor cu parametri Angajat b("Ion Padure","Manager",1992,1125.75); b.Afisare();

12

Page 13: POO - Curs Doc

Se afişează:Nume: Ion Padure | Functie: Manager | Anul nasterii: 1992 | Salariu: 1125.75

4) // obiect creat cu al 2-lea constructor cu parametri Angajat c("Dan Balan",1984); c.Afisare();

Se afişează:Nume: Dan Balan | Functie: - | Anul nasterii: 1984 | Salariu: 0

5) // obiect creat cu al 3-lea constructor cu parametri Angajat d(756.45); d.Afisare();

Se afişează:Nume: Necunoscut | Functie: - | Anul nasterii: 0 | Salariu: 756.45

6) // obiect creat cu constructorul de copiere Angajat e(b); e.Afisare();

Se afişează:Nume: Ion Padure | Functie: Manager | Anul nasterii: 1992 | Salariu: 1125.75

7) c.SalariuMediu(b,d); c.Afisare();

Se afişează:Nume: Dan Balan | Functie: - | Anul nasterii: 1984 | Salariu: 941.1

8) Aceste mesaje apar după ce se termină lucrul programului.Destructorii se apelează automat de către sistem pentrufiecare obiect creat din program, în ordine inversă:

Obiectul (Ion Padure) a fost distrus.Obiectul (Necunoscut) a fost distrus.Obiectul (Dan Balan) a fost distrus.Obiectul (Ion Padure) a fost distrus.Obiectul (Petru Moraru) a fost distrus.

§4. Operaţii de intrare/ieşire a informaţiei.

În limbajul C++ sunt admise toate operaţiile de intrare/ieşire caracteristice limbajului C. Suplimentar, în limbajul C++ au fost definite operaţii noi de intrare/ieşire care sunt create în stilul programării orientate pe obiecte. Au fost create o serie de clase orientate la intrarea/ieşirea informaţiei. În baza acestor clase sunt definite obiecte care, la rândul său, includ operaţii ce permit intrarea/ieşirea informaţiei. O astfel de abordare şi stil de realizare, are avantajul de a putea fi extinsă pentru necesităţile unei clase concrete.

Pentru ieşirea sau afişarea informaţiei, este definit obiectul cout, iar pentru intrarea sau citirea informaţiei, este definit obiectul cin. Pentru afişarea mesajelor despre erori sunt definite obiectele cerr şi clog. Fiecare dintre obiecte poate utiliza atât operatorii, cât şi funcţiile caracteristice clasei din care este instanţiat el. În contextul obiectelor cout, cerr şi clog este utilizat operatorul <<, numit operator de inserţie sau scriere, iar în contextul obiectului cin este utilizat operatorul >>, numit operator de extragere sau citire. O formă generală de utilizare a operatorului << ar fi următoarea:

cout<<exp1<<exp2<<...<<expN;

13

Page 14: POO - Curs Doc

unde exp1, exp2, …, expN (N1) sunt expresiile a căror valori sunt afişate. Expresiile concatenate într-o expresie lungă pot fi afişate separat:cout<<exp1;cout<<exp2;. . .cout<<expN;

Un exemplu de afişare a valorilor unor variabile:int i=-27;char c=’a’;char s[]=”Un sir”;long l=100000;

cout<<i;cout<<c;cout<<s;cout<<l;

Rezultatul afişării:-27aUn sir100000

Desigur că scriind:cout<<i<<c<<s<<l;

va fi obţinut acelaşi rezultat al afişării.

Pentru operatorul de citire >> este valabilă următoarea formă generală:cin>>var1>>var2>>...>>varN;

unde var1, var2, …, varN (N1) sunt variabilele în care sunt citite valori.

Acelaşi lucru poate fi realizat şi în expresii separate:cin>>var1;cin>>var2;. . .cin>>varN;

Un exemplu de utilizare a obiectului cin: unsigned u;float f;long double ld;

cin >> u >> f >> ld;

Delimitatori ai valorilor introduse sunt spaţiile albe ’ ’, ’\t’, ’\n’.Pentru a putea utiliza în programe obiectele şi operatorii respectivi, este inclus fişierul antet

<iostream.h>, unde sunt efectuate descrierile necesare referitoare la aceste obiecte.Obiectele cout şi cin permit, de asemenea, şi operaţii de formatare care sunt realizate prin

intermediul unor construcţii numite manipulatori. Un manipulator schimbă starea fluxului pregătindu-l pentru careva operaţii de formatare. Pentru a putea folosi manipulatorii, este necesară includerea fişierului antet <iomanip.h>. În caz general, un manipulator acţionează asupra fluxului, acţionând asupra obiectului implicat în operaţiile de intrare/ieşire prin intermediul operatorului corespunzător: cout << manipulator[ << . . .];

saucin >> manipulator[ >> . . .];

unde punctele de suspensie ”. . .” înseamnă o posibilă continuare a construcţiei.

Fluxul de intrare are mai puţini manipulatori decât cel de ieşire. Pentru ignorarea spaţiilor albe la citirea informaţiei, este utilizat manipulatorul ws. În exemplul următor:

14

Page 15: POO - Curs Doc

double d;cin >> ws >> d;

sunt ignorate spaţiile albe în procesul citirii valorii în variabila d.

În continuare, vor fi caracterizaţi manipulatorii fluxului de ieşire. Pentru a trece din rând nou la ieşirea (afişarea) informaţiei, este utilizat manipulatorul endl. Din exemplul ce urmează se poate vedea că acest manipulator poate fi modelat afişând secvenţa de dirijare ’\n’:int i=10;float f=1.2;char s[]=”Un exemplu”, c=’A’;cout<<i<<endl<<f<<endl<<c<<”\n”<<s<<endl;

Rezultatul afişării:101.2AUn exemplu

În continuare urmează un grup de manipulatori care se referă doar la tipuri de date întregi:- oct permite fixarea sistemului de numeraţie octal;- dec permite fixarea sistemului de numeraţie zecimal;- hex permite fixarea sistemului de numeraţie hexazecimal.

Oricare dintre aceşti manipulatori îşi fixează acţiunea până la anularea ei de un alt manipulator din acest grup.int i=10;cout<<i<<” ”<<oct<<i<<” ”<<hex<<i<<” ”<<dec<<i<<endl;

Rezultatul afişării:10 12 a 10

Manipulatorii descrişi anterior sunt manipulatori ne-parametrizaţi. Există o serie de manipulatori parametrizaţi, a căror descriere urmează în continuare. Ei sunt realizaţi sub formă de funcţii, deci, modul lor de utilizare este asemănător cu apelarea unor funcţii.

Un manipulator a cărui acţiune este echivalentă cu acţiunea manipulatorilor oct, dec, hex este manipulatorul setbase. Modul lui de utilizare este setbase(baza)

unde parametrul baza poate lua valorile 8, 10 sau 16, permiţând respectiv fixarea sistemului de numeraţie octal, zecimal sau hexazecimal. Va fi rescris exemplul anterior utilizând setbase:int i=10;cout<<i<<” ”<<setbase(8)<<i<<” ”<<setbase(16)<<i<<” ”<<setbase(10)<<i<<endl;

Rezultatul afişării:10 12 a 10

Dacă este necesară fixarea lăţimii câmpului de afişare, este utilizat manipulatorul setw. Forma generală este:setw(latime)

unde parametrul latime reprezintă o expresie întreagă care dictează lăţimea câmpului de afişare. Lăţimea câmpului este fixată doar pentru o singură afişare, după care se revine la valoarea implicită egală cu unu.

Un exemplu de utilizare a acestui manipulator:int i=10, lat=5;char c=’A’;cout<<setw(3)<<i<<setw(lat)<<c<<endl;

Rezultatul afişării (fiecare celulă reprezintă o poziţie pe ecran): 1 0 A

15

Page 16: POO - Curs Doc

Din exemplul precedent se vede că dacă valoarea afişată are mai puţine simboluri decât numărul de poziţii ale câmpului de afişare, atunci poziţiile rămase sunt completate cu spaţii ’ ’. Acesta este caracterul implicit de completare. Însă este posibilă fixarea altui caracter de completare, utilizând manipulatorul setfill. Forma generală este:setfill(caracter_completare)

unde parametrul caracter_completare reprezintă o expresie, care oferă caracterul de completare. Caracterul de completare este fixat până la o nouă fixare. În continuare este re-scris exemplul precedent, utilizând şi manipulatorul setfill:int i=10, lat=5;char c=’A’;cout<<setfill(’.’);cout<<setw(3)<<i<<setw(lat)<<c<<endl;

Rezultatul afişării:. 1 0 . . . . A

Pentru a fixa exactitatea de afişare a valorilor cu virgulă mobilă este utilizat manipulatorul setprecision. Forma generală de utilizare este următoarea:setprecision(exactitate)

unde parametrul exactitate este o expresie întreagă, care dictează numărul de cifre semnificative după virgulă. Exactitatea este fixată pentru toate afişările, până la o nouă fixare. Dacă parametrul exactitate ia valoarea 0, atunci se revine la exactitatea implicită egală cu 6 cifre după virgulă. Iată un exemplu:float f=1.2;double d=-15.4563283432;int i;

cout << f << ” ” << d << endl;for(i=0;i<6;i++) cout<<setprecision(i)<<f<<” ”<<d<<endl;

Rezultatul afişării:1.2 -15.4563281.2 -15.4563281.2 -15.51.2 -15.461.2 -15.4561.2 -15.45631.2 -15.45633

Un manipulator cu o gamă largă de acţiuni de formatare este manipulatorul setiosflags. Forma generală de aplicare a acestui manipulator este:setiosflags(fanioane)

unde parametrul fanioane permite fixarea unor proprietăţi, numite fanioane. Fiecare fanion este responsabil de o anumită acţiune. Fanioanele, utilizate cu manipulatorul setiosflags sunt descrise sub formă de enumerare în cadrul clasei ios. De aceea denumirile fanioanelor sunt precedate de denumirea clasei ios şi operatorul rezoluţiei.

Tabelul următor descrie acţiunile, ce ţin de fiecare fanion:Nr. Nume fanion Acţiune caracteristică1 ios::skipws ignorarea spaţiilor albe2 ios::left alinierea informaţiei în

partea stângă a câmpului3 ios::right alinierea informaţiei în

partea dreaptă a câmpului4 ios::internal alinierea informaţiei în

ambele părţi dacă este posibilă

5 ios::showpos forţarea semnului6 ios::showbase baza sistemului de

numeraţie

16

Page 17: POO - Curs Doc

7 ios::showpoint forţarea punctului pentru numerele cu virgulă mobilă

8 ios::fixed afişarea numerelor reale în format cu punct fix

9 ios::scientific afişarea numerelor reale în format ştiinţific

10 ios::hex afişarea numerelor întregi în format hexazecimal

11 ios::oct afişarea numerelor întregi în format octal

12 ios::dec afişarea numerelor întregi în format zecimal

13 ios::stdio golirea fluxul stdout şi stderr

14 ios::uppercase scrierea numerelor hexazecimale cu litere majuscule

15 ios::unitbuf golirea tuturor fluxurilor de date dupa citirea datelor

Iată o serie de exemple ce descriu acţiunile manipulatorului setiosflags.Alinierea stânga sau dreapta:int i=10;cout<<setiosflags(ios::left)<<setw(5)<<i <<setfill(’-’)<<setiosflags(ios::right) <<setw(5)<<i<<endl;

Rezultatul afişării:1 0 - - - 1 0

Forţarea semnului şi a punctului:float f=20;cout<<f<<’ ’<<setiosflags(ios::showpos)<<f<<’ ’ <<setiosflags(ios::showpoint)<<f<<’ ’<<endl;

Rezultatul afişării:2 0 + 2 0 + 2 0 . 0 0 0 0 0 0

Manipulatorul setiosflags permite combinarea mai multor fanioane concomitent. Pentru aceasta este utilizat operatorul | (SAU). Iată un exemplu: long l=0x49;cout << setiosflags(ios::showbase | ios::hex);cout << setw(6) << l << setiosflags(ios::internal) << setw(6) << l << endl;

Rezultatul afişării: 0 x 4 9 0 x 4 9

Pentru a restabili fanioanele la valorile implicite, este utilizat manipulatorul resetiosflags, care acţionează similar cu manipulatorul setiosflags. În exemplul următor este restabilit fanionul ios::showpoint:double d=-320;cout <<setiosflags(ios::showpoint) << d << ’ ’ <<resetiosflags(ios::showpoint) << d <<endl;

Rezultatul afişării:- 3 2 0 . 0 0 0 0 0 0 - 3 2 0

Clasele orientate la intrarea/ieşirea informaţiei au pe lângă operatori şi o serie de funcţii cu acţiuni specifice, utile în procesul de intrare/ieşire a informaţiei. Astfel, funcţiile corespunzătoare pot fi utilizate cu obiectele cout şi cin.

17

Page 18: POO - Curs Doc

Pentru a citi un caracter din fluxul de intrare, este utilizată funcţia get(). Forma de utilizare este următoareacin.get(caracter);

unde parametrul caracter este variabila în care este citită valoarea. De exemplu:char c;cin.get(c);

Operatorul de citire >> a obiectului cin nu permite un lucru eficient cu şiruri de caractere. De exemplu, dacă se doreşte a citi numele şi prenumele unei persoane într-o variabilă, atunci o astfel de soluţie char persoana[100];cin >> persoana;

nu este corectă, fiindcă în variabila persoana va fi citit doar numele persoanei, prenumele fiind ignorat, fiindcă spaţiul joacă rolul de trecere la prelucrarea următorului câmp de intrare. Pentru a putea citi propoziţii, va fi utilizată funcţia getline(), care are următoarele forme:getline(char *sir, int lung_max);getline(char *sir, int lung_max, char caracter_oprire);

unde parametrul sir este şirul în care se citeşte, parametrul lung_max este lungimea maximală a informaţiei citite, iar parametrul caracter_oprire reprezintă caracterul la întâlnirea căruia se va întrerupe procesul de citire. Pentru prima variantă a funcţiei, caracterul de oprire este ’\n’, generat de tasta Enter. Astfel sarcina propusă anterior poate fi soluţionată aşa:char persoana[100];cin.getline(persoana, sizeof(persoana));

Dacă este necesar de a afla câte caractere au fost citite în fluxul neformatat este aplicată funcţia gcount().char nuA[50];int i;cin.getline(nuA, sizeof(nuA), ’a’);i=gcount();cout << i << endl;

Fragmentul anterior afişează câte simboluri au fost citite în variabila nuA până la întâlnirea primului simbol ’a’.

Desigur că şi obiectul cout are o serie de funcţii utile. Funcţia width() are două variante. Una permite citirea lăţimii curente, iar alta permite fixarea lăţimii curente. Iată forma lor generală:int width();

şi int width(latime);

unde latime este expresia a cărei valoare va fi luată drept bază pentru fixarea lăţimii câmpului de afişare. De exemplu: float f=10.25;cout.width(8);cout<<f<<endl;

Rezultatul afişării: 1 0 . 2 5

Două forme similare are şi funcţia precision(), care se referă la exactitatea de afişare a valorilor cu virgulă mobilă: varianta fără parametri returnează exactitatea curentă, iar varianta cu parametru fixează exactitatea necesară. De exempludouble d=-114.361782;cout.precision(4);cout<<d<<endl;

Rezultatul afişării:- 1 1 4 . 3 6 1 8

18

Page 19: POO - Curs Doc

Pentru a determina caracterul de umplere a poziţiilor ne-acoperite de simbolurile valorii afişate poate fi utilizată funcţia fill() fără parametri. De exemplu, fragmentul ce urmează:char cu;cu=cout.fill();cout << cu << endl;

va afişa caracterul de umplere curent cu, determinat utilizând funcţia fill(). Pentru a fixa caracterul de umplere, este utilizată, de asemenea, funcţia fill(caracter_umplere) cu parametri, având ca parametru o expresie ce oferă caracterul de umplere. De exemplu, în cele ce urmează va fi fixat caracterul de umplere ’*’.long l=-249793;cout.fill(’*’);cout.width(10);cout<<l<<endl;

Rezultatul afişării:* * * - 2 4 9 7 9 3

Pentru a putea lucra cu fanioanele caracterizate anterior, poate fi utilizată funcţia flags(), care are o variantă fără parametri şi o variantă cu parametri. int fnv = cout.flags();int fn = 0x1234;cout.flags(fn);cout << 239 << endl;cout.flags(fnv);

În acest exemplu sunt utilizate ambele variante, care permit fixarea valorii vechi a fanioanelor şi revenirea la această valoare după afişarea valorii întregi 239.

Exemplu. De alcătuit un program în care este modelat un cuprins de carte.#include<iostream.h>#include<iomanip.h>#include<string.h>#define latimePagina 65

void main(){ char *paragrafe[]={”Cuprins”,”Introducere”, ”Clase si obiecte”, ”Constructor.Destructor”}; int pagini[]={3,10,12}; int i, lung;

lung=strlen(paragrafe[0]); cout << setw((latimePagina+lung)/2); //Centrare cout << paragrafe[0] << endl; //Cuprins

cout << setfill(’.’);

for(i=0; i<3; i++) {

cout << paragrafe[i+1];lung=strlen(paragrafe[i+1]);cout << setw(latimePagina-lung);cout << pagini[i] << endl;

}}

Rezultatul afişării: CuprinsIntroducere.....................................................3Clase si obiecte...............................................10Constructor.Destructor.........................................12

19

Page 20: POO - Curs Doc

§5. Moştenire simplă.

Obiectele din domeniul de problemă nu sunt statice unul faţă de altul, ci inter-acţionează între ele. Vom spune că obiectele din cadrul unei probleme se află în anumite relaţii. Vom evidenţia o serie de relaţii dintre obiecte:

1. relaţii de asociere, când un obiect transmite un mesaj altui obiect;2. relaţii de agregare, când un obiect este parte componentă a altui obiect;3. relaţii de tip client-server, când un obiect foloseşte serviciile oferite de alt obiect;4. relaţii de moştenire, când un obiect transmite o serie de proprietăţi şi operaţii altui obiect.

Fiindcă obiectele de acelaşi tip formează o clasă de obiecte, se poate vorbi şi despre relaţii între clase, generalizându-le pe cele enumerate anterior la nivel de clase.

Utilizând relaţia de moştenire în procesul programării se obţin aplicaţii eficiente din punctul de vedere al mărimii codului obţinut şi al lipsei de erori. De exemplu, trebuie de realizat un sistem de gestiune în cadrul sistemului de fişiere. În domeniul de problemă pot fi evidenţiate o serie de obiecte caracteristice. Ca exemplu ar putea fi evidenţiate obiectele Program şi Document. Dacă ar fi proiectate tipuri abstracte de date corespunzătoare acestor obiecte, evidenţiind proprietăţile şi operaţiile caracteristice acestor obiecte, s-ar putea propune următoarele clase:

Document ProgramNumeDimensiuneDataTip

NumeDimensiuneDataTipSistemOperare

CopiereNimicireRedenumireAfişareImprimare

CopiereNimicireRedenumireLansare

Analizând clasele propuse, se poate observa că există o serie de proprietăţi şi o serie de operaţii comune pentru ambele tipuri de obiecte. Definind funcţiile membre ale ambelor clase, vom avea o serie de funcţii din diferite clase realizate identic. Astfel, se obţine o dublare de cod. O cale mai eficientă ar fi posibilitatea de evidenţiere şi implementare a unei clase generale, care ar putea transmite caracteristicile sale altor clase mai concretizate. Astfel în cazul exemplului de mai sus s-ar putea evidenţia obiectul general numit Fisier, care va conţine caracteristicile comune ale obiectelor ce ţin de sistemul de fişiere:

FisierNumeDimensiuneDataCopiereNimicireRedenumire

Transmiterea de proprietăţi şi funcţionalităţi dintr-o clasă numită clasă de bază într-o clasă numită clasă derivată se numeşte moştenire simplă. Procesul de transmitere se numeşte derivare.

La nivel de diagramă, moştenirea simplă are următoarea reprezentare:

20

Document ProgramTip TipSistemOperareAfisareImprimare

Lansare

Page 21: POO - Curs Doc

La nivel de descriere în limbajul C++, avem următoarea formă generală:class cl_baza{.<proprietati si metode cl_baza>.};

class cl_der1:[modificator_de_mostenire1] cl_baza{.<proprietati si metode cl_der1>.};

...

class cl_derN:[modificator_de_mostenireN] cl_baza{.<proprietati si metode cl_derN>.};

unde elementele modificator_de_mostenire1, …, modificator_de_mostenireN reprezintă modificatorii de moştenire prin care clasa de bază este implicată în procesul de derivare. Modificatorii de moştenire pot fi unul dintre cuvintele-cheie:privateprotectedpublic

Dacă modificatorul de moştenire nu este prezent în descriere, se consideră implicit modificatorul private.

Urmând schema generală, pentru exemplul anterior, s-ar obţine următoarea descriere:class Fisier

{char Nume[256];unsigned long Dimensiune;char Data[20];

public:void Nimicire();void Copiere(char *numeFisier);void Redenumire(char *numeNou);};

//---------------class Document : public Fisier

{char Tip[15];

public:void Afisare();void Imprimare();};

//---------------

cl_derN. . .cl_der2cl_der1

cl_baza

21

Page 22: POO - Curs Doc

class Program : public Fisier{char TipSistemOperare[25];

public:void Lansare();};

Un obiect creat în baza clasei derivate are o serie de caracteristici dependente de clasa de bază. De aceea, în procesul de creare a acestui obiect, va participa atât constructorul clasei de bază, cât şi constructorul clasei derivate. Acest fapt este vizibil şi la nivel de descriere generală a constructorului clasei derivate:cl_der::cl_der(param_cl_der):cl_baza(param_cl_baza)

{.//instructiuni.}

De obicei, lista parametrilor clasei de bază este o submulţime a listei parametrilor clasei derivate.Fiindcă procesul de creare a unui obiect în baza clasei derivate depinde de doi constructori, este bine

a şti ordinea execuţiei lor: mai întâi va lucra constructorul clasei de baza, iar apoi va lucra constructorul clasei derivate.

Ordinea de execuţie a destructorilor este inversă: mai întâi lucrează destructorul din clasa derivată, iar apoi lucrează destructorul din clasa de bază.

A fost menţionat că o clasă derivată are o serie de caracteristici care vin din clasa de bază (le moşteneşte). Vom examina tipul de acces la aceşti membri moşteniţi în clasa derivată. Combinaţia dintre modificatorii de acces ai membrilor clasei de bază şi modificatorul de moştenire determină tipul de acces la membrii clasei derivate.

Tabelul de mai jos descrie combinaţiile posibile şi tipul de acces obţinut ca rezultat:Modificator de moştenire

private protected publicModificator de acces (în clasa de bază)

private inaccesibil inaccesibil inaccesibilprotected private protected protectedpublic private protected public

Exemplu. De alcătuit un program în care sunt implementate clasa punct şi clasa cerc, care este derivată din clasa punct.

class punct{protected:

int x, y;int visibil;

public:punct(int x1, int y1);void afisare();void stingere();void miscare(int xn, int yn);

};

class cerc : public punct{

int r;public:

cerc(int x, int y, int r);void afisare_c();void stingere_c();void miscare_c(int xn, int yn);

};//------------------------

22

Page 23: POO - Curs Doc

punct :: punct(int x1, int y1){

x=x1;y=y1;

}//------------------------void punct :: afisare()

{ if (visibil == 0){

putpixel(x, y, getcolor() );visibil=1;

}}

//------------------------void punct :: stingere()

{ if (visibil==1){

putpixel(x, y, getbkcolor() );visibil=0;

} }

//------------------------void punct :: miscare(int xn, int yn){

stingere();x=xn;y=yn ;afisare();

}//------------------------cerc :: cerc(int x, int y, int r1): punct (x,y){ r=r1; }//------------------------void cerc :: afisare_c(){

if (visibil ==0){

circle(x,y,r);visibil=1;

}}//------------------------void cerc :: stingere_c(){

int c=getcolor();if (visibil == 1){

setcolor(getbkcolor());circle(x,y,r);setcolor(c);visibil=0;

}}//------------------------void cerc :: miscare_c(int xn, int yn){

stingere_c();x=xn;y=yn;afisare_c();

}

23

Page 24: POO - Curs Doc

Declararea claselor şi implementarea lor vor fi memorate în fişierul cerc.hpp. Programul principal în care sunt utilizate obiecte create în baza clasei cerc va fi scris în fişierul cerc.cpp://cerc.cpp#include<iostream.h>#include<conio.h>#include<graphics.h>#include ”cerc.hpp”void main(){

int dr = DETECT, rdr, er;initgraph(&dr, &rdr, ””);er=graphresult();

if (er==grOk){

cerc c1(100, 100, 20);punct p(150, 270);p.afisare();c1.afisare_c();c1.miscare_c(200,300);getch();

}else

cout << ”Eroare de initializare a regimului grafic.\n”;closegraph();

}

§6. Moştenire multiplă

Transmiterea de proprietăţi şi funcţionalităţi dintr-o serie de clase numite clase de bază într-o clasă numită clasă derivată se numeşte moştenire multiplă. Procesul de transmitere se numeşte derivare.

Diagrama ce urmează descrie schema generală a moştenirii multiple:

La nivel de descriere în expresii ale limbajului, avem următoarea schemă:

class cl_baza1{.<date si functii membre 1>.};

class cl_baza2{.<date si functii membre 2>.};

. . .class cl_bazaN

{.

cl_der

cl_bazaN. . .cl_baza2cl_baza1

24

Page 25: POO - Curs Doc

<date si functii membre N>.};

class cl_der : [mod_m1]cl_baza1, ..., [mod_mN]cl_bazaN{.<date si functii membre Der>.};

unde cl_baza1, cl_baza2, …, cl_bazaN sunt clasele de bază, cl_der este clasa derivată, mod_m1, mod_m2, ..., mod_mN sunt modificatorii de moştenire care introduc clasele de bază în procesul de derivare. Ca şi în cazul moştenirii singulare modificatori de moştenire pot fi unul dintre cuvintele-cheie: public, protected, private. Dacă o oarecare clasă de bază nu are scris explicit modificator de moştenire, se consideră implicit modificatorul private.

Un obiect creat în baza clasei derivate va avea o serie de caracteristici dependente de clasele de bază din care vin, ceea ce este vizibil şi din modul general de descriere a constructorului clasei derivate:cl_der::cl_der(param_der)[:cl_baza1(param_b1)],... [,cl_bazaN(param_bN)]

{.//instructiuni.}

Întrucât, la crearea unui obiect în baza unei clase derivate conlucrează mai mulţi constructori, va fi evidenţiată ordinea de execuţie a lor. Mai întâi sunt executaţi constructorii claselor de bază în ordinea în care apar ei în descrierea clasei derivate. După ce constructorii claselor de bază şi-au terminat lucrul, este executat şi constructorul clasei derivate. Destructorii sunt executaţi în ordine strict inversă ordinii de execuţie a constructorilor.

Pentru a caracteriza interacţiunea dintre constructorii clasei derivate şi constructorii claselor de bază, concretizând astfel descrierea generală a constructorilor clasei derivate vor fi evidenţiate următoarele patru situaţii posibile:

1) Există măcar un constructor în clasa derivată şi constructori măcar în unele clase de bază. În acest caz constructorii claselor de bază pot să lipsească din descrierea constructorului clasei derivate doar când sunt fără parametri sau se reduc la constructori fără parametri.2) Există măcar un constructor în clasa derivată şi nu există constructori în clasele de bază. În acest

caz, fiindcă constructorii impliciţi nu fac careva iniţializări, ar putea fi deschis accesul spre unii membri ai claselor de bază pentru a putea fi iniţializaţi din constructorul clasei derivate.3) Nu există constructori în clasa derivată, dar există constructori măcar în unele clase de bază. În

acest caz clasele de bază care au constructori trebuie să aibă constructori fără parametri sau constructori care se reduc la constructori fără parametri.4) Nu există constructori în clasa derivată şi nu există constructori în nici o clasă de bază.

Lista parametrilor fictivi a constructorului clasei derivate adeseori acoperă necesitatea de parametri ai constructorilor claselor de bază, dar aceasta nu este o cerinţă strictă.

Unii membri ai claselor de bază pot fi supraîncărcaţi (suprascrişi) în procesul derivării. Existenţa în clasa derivată a unor membri cu aceleaşi caracteristici ca şi în clasa de bază va fi numită supraîncărcare. În cazul datelor membre caracteristicile vor însemna:

- numele membrului;- tipul membrului.

În cazul funcţiilor membre caracteristicile vor însemna prototipul funcţiei membru, adică:- numele membrului;- tipul returnat;- tipul şi numărul parametrilor fictivi ai funcţiei.

25

Page 26: POO - Curs Doc

Carte Discc

Suport informational

Este posibilă utilizarea membrilor supraîncărcaţi ai claselor de bază în cadrul funcţiilor membre ale clasei derivate. Pentru a putea accesa un membru supraîncărcat al unei clase de bază, numele lui este prefixat de numele clasei conectat prin operatorul rezoluţiei:

nume_cl_baza::nume_membru

O situaţie similară are loc şi atunci când în, cel puţin, două clase de bază sunt membri cu aceleaşi caracteristici. Pentru a putea accesa careva dintre aceşti membri în cadrul funcţiilor membre ale clasei derivate, numele membrului este, de asemenea, prefixat de numele clasei conectat prin operatorul rezoluţiei.

Exemplu. În prezent, o serie de cărţi sunt dotate cu un disc conţinând conţinutul cărţii în varianta electronică. Exemplul ce urmează se va referi la o astfel de situaţie. Diagrama de moştenire este următoarea:

Realizare. De alcătuit un program în care sunt implementate clasele carte, disc şi supInfo, care este derivată din clasele carte şi disc.class carte{

char *autor;char *titlu;int an;

public:carte(char *au, char *t, int a);carte();~carte();void afisare();

};class disc{

char *tip;int capacitate;

public:disc(char *t, int c);~disc();void afisare();

};class supInfo: public carte, public disc{

float pret;public:

supInfo(char *au, char *tt, int a, char *t, int c, float p);

supInfo(char *t, int c, float p);void afisare();

};//===================carte::carte(char *au, char *t, int a)

{autor=(char *)malloc(strlen(au)+1);titlu=(char *)malloc(strlen(t)+1);strcpy(autor, au);strcpy(titlu, t);an=a;

26

Page 27: POO - Curs Doc

}//-------------------carte::carte()

{ char sir[100]; cout<<”Autorul: ”; cin.getline(sir, 100); autor=(char *)malloc(cin.gcount()); strcpy(autor, sir); cout<<”Titlul: ”; cin.getline(sir, 100); titlu=(char *)malloc(cin.gcount()); strcpy(titlu, sir); cout<<”Anul: ”; cin>>an;

}//-------------------carte::~carte()

{free(autor);free(titlu);}

//-------------------void carte::afisare()

{cout<<titlu<<” de ”<<autor<<” a aparut in ”<<an<<endl;}

//===================disc::disc(char *t, int c)

{tip=(char *)malloc(strlen(t)+1);strcpy(tip, t);capacitate=c;}

//-------------------disc::~disc()

{free(tip);}

//-------------------void disc::afisare()

{cout<< ”Are un disc ” << tipul

<< ” de capacitatea” << capacitate << endl;

}//===================supInfo::supInfo(char *au, char *tt, int a, char *t, int c, float p):

carte(au,tt,a), disc(t,c){pret=p;}

//-------------------supInfo::supInfo(char *t, int c, float p):

disc(t,c){pret=p;}

//-------------------void supInfo::afisare()

{carte::afisare();

27

Page 28: POO - Curs Doc

disc::afisare();cout<<”Si are un pret de ”<<pret<<” lei”<<endl;}

Declararea claselor şi implementarea lor va fi memorată în fişierul supinfo.hpp. Programul principal în care sunt utilizate obiecte create în baza clasei supInfo va fi scris în fişierul supinfo.cpp:#include<iostream.h>#include<string.h>#include<stdlib.h>#include ”supinfo.hpp”

void main(){ supInfo cpp(”Kris Jansa”, ”Succes cu C++”, 2000, ”CD-ROM”, 700, 200); supInfo pr(”CD-ROM”,700,300); cpp.afisare(); pr.afisare();}

§7. Moştenirea pe mai multe niveluri. Clase virtuale

Atunci când o clasă transmite parametri sau funcţionalităţi altei clase care, la rândul său, se consideră clasă de bază pentru o altă ierarhie de moştenire, vom spune că avem moştenire pe mai multe niveluri.

Cea mai simplă diagramă care exemplifică moştenirea pe mai multe niveluri este următoarea:

Schema de moştenire pe mai multe niveluri se reduce la o aplicare consecutivă a schemei de moştenire pe un singur nivel. Schemele de moştenire pe mai multe niveluri pot fi reprezentate prin grafuri cu mai multe niveluri. Diagramele reprezentate prin grafuri care nu conţin cicluri nu prezintă careva dificultăţi de realizare. Dacă însă grafurile au cicluri, apar unele probleme. Cea mai simplă diagramă de acest fel este de următorul tip:

D

CB

A

cl_der1

cl_der

cl_baza

28

Page 29: POO - Curs Doc

Pentru o astfel de diagramă există şi interpretări care descriu situaţii reale:

La realizarea unei astfel de scheme, membrii din clasa Persoana vor ajunge în clasa Doctornad pe două căi: una prin clasa Asistent şi cealaltă prin clasa Student. Deci clasa Doctorand va avea două exemplare pentru fiecare membru venit din clasa Persoana. Generalizând cele spuse anterior, s-ar putea afirma că dacă dintr-un vârf ierarhic superior al diagramei există mai multe drumuri către un alt vârf ierarhic inferior, atunci fiecare membru al vârfului ierarhic superior va ajunge în vârful ierarhic inferior de atâtea ori câte drumuri există între aceste vârfuri.

Uneori, în situaţia apariţiei mai multor membri care vin în aceeaşi clasă, apar situaţii de conflict. S-ar putea evita astfel de situaţii, prin utilizarea claselor virtuale la moştenire. Pentru a declara o clasă virtuală la moştenire în schema de moştenire se utilizează cuvântul-cheie virtual înaintea clasei date. Clasele virtuale sunt protejate de dublări ale membrilor.

În continuare, vor fi exemplificate ideile expuse anterior utilizând diagrama precedentă care descrie relaţia dintre clasele A, B, C, D.#include<iostream.h>class A{protected:

int ia;public:

A(int i){ia=i;}};//-------------class B:public A{protected:

int ib;public:

B(int ia, int i) : A(ia){ ib=i; }};//-------------class C:public A{protected:

int ic;public:

C(int ia, int i) : A(ia){ ic=i; }};//-------------class D:public B, public C{

int id;public:

D(int ia, int ib, int ic, int i) : B(ia, ib),C(ia, ic){ id=i; }

void afisare()

Doctorand

AsistentStudent

Persoana

29

Page 30: POO - Curs Doc

{ cout<<”ia=”<<ia<<”ib=”<<ib<<”ic=”<<ic <<”id=”<<id<<endl; }};//==============void main(){

D d(7,17,27,37);d.afisare();

}

Exemplul anterior are o problemă: funcţia membru afisare(), apelată prin intermediul obiectului d, va genera o eroare din motiv că membrul ia nu poate fi afişat, deoarece în clasa D există doi membri cu numele ia. Va trebui schimbat modul de afişare după cum urmează: void afisare() { cout<<”ia=”<<B::ia<<”ib=”<<ib<<”ic=”<<ic <<”id=”<<id<<endl; }

afişând membrul ia ce vine prin clasa B. După această schimbare, programul va putea fi lansat, dar clasa D va continua să aibă doi membri cu numele ia. Pentru a scăpa de acest neajuns, clasa A va fi declarată clasă virtuală.

Declarând clasa A clasă virtuală, va fi făcută o schimbare mică, dar esenţială a exemplului. Iată ce se obţine:#include<iostream.h>class A{protected:

int ia;public:

A(int i){ia=i;}};//-------------class B:public virtual A{protected:

int ib;public:

B(int ia, int i) : A(ia){ ib=i; }};//-------------class C:virtual public A{protected:

int ic;public:

C(int ia, int i) : A(ia){ ic=i; }};//-------------class D:public B, public C

{int id;

public:D(int ia, int ib, int ic, int i) :

B(ia, ib),C(ia, ic){ id=i; } void afisare() { cout<<”ia=”<<ia<<”ib=”<<ib<<”ic=”<<ic <<”id=”<<id<<endl; }};//==============

30

Page 31: POO - Curs Doc

void main(){D d(7,17,27,37);d.afisare();}

Ordinea de execuţie a constructorilor în scheme de moştenire ce conţin clase virtuale este următoarea: sunt evidenţiate clasele virtuale şi sunt executaţi constructorii lor în ordinea în care sunt întâlniţi în descrierea clasei derivate. După ce îşi termină lucrul constructorii virtuali, vor lucra constructorii ne-virtuali ai claselor ne-virtuale, tot în ordinea apariţiei în schema de moştenire.

Membrii transmişi din clasa de bază în clasa derivată au tendinţa de a-şi micşora gradul de acces din punctul de vedere al clasei derivate. În scheme de moştenire pe mai multe niveluri această micşorare poate fi şi mai accentuată, existând posibilitate de existenţă a membrilor din clasa de bază, care ajung inaccesibili într-o oarecare clasă derivată de la anumit nivel. Există posibilitatea de a mări gradul de acces al membrilor transmişi dintr-o clasă de bază într-o clasă derivată. Pentru a mări gradul de acces al unui membru transmis dintr-o clasă de bază într-o clasă derivată, numele membrului precedat de numele clasei şi operatorul rezoluţiei sunt plasate în raza de acţiune a modificatorului de acces necesar. modificator_acces: . . . nume_cl::nume_mem;

Trebuie de ţinut cont că gradul de acces poate fi mărit readucându-l la acelaşi grad pe care la avut în clasa de bază. Nu sunt permise gradaţii de acces intermediare.

De exemplu, fie clasa derivata, care este derivată din clasa baza: class baza{public:

int ib;};//--------class derivata : private baza{

int id;public:

baza::ib;};

Membrul ib în clasa derivata va avea gradul de acces private şi îl readucem la gradul de acces public, pe care îl avea în clasa baza. Nu este posibilă atribuirea gradului de acces intermediar protected.

§8. Specificul utilizării funcţiilor în C++

Caracteristicile impuse funcţiilor de limbajul C sunt, în mare parte, respectate şi în cadrul limbajului C++. Dar există şi o serie de caracteristici noi. Un mecanism important este posibilitatea de supraîncărcare a funcţiilor. Acest mecanism permite o eficienţă sporită la nivel de elaborare şi întreţinere a programelor.

Funcţiile care urmează permit sumarea elementelor unui tablou de tip float şi elementelor unui tablou de tip int.float suma1(float *tf, int n_e){

float s=0;for (int i=0; i<n_e; i++)

s+=tf[i];return s;

}

int suma2(int *ti, int n_e)

31

Page 32: POO - Curs Doc

{ int s=0;for (int i=0; i<n_e; i++)

s+=ti[i];return s;

}

Crearea de funcţii cu denumiri diferite, dar care realizează algoritmi similari pentru diferite tipuri ale parametrilor fictivi, poate genera probleme la nivel de apelare, fiindcă trebuie apelată funcţia corectă pentru combinaţia dată de parametri reali. Este necesară atenţie din partea programatorului, fiindcă apelarea funcţiei suma1() - pentru un tablou cu elemente întregi, sau a funcţiei suma2() - pentru un tablou cu elemente reale, va genera rezultate incorecte. O posibilă simplificare pentru astfel de situaţii ar fi crearea de funcţii cu acelaşi nume şi transmiterea responsabilităţii de determinare a variantei corecte a funcţiei apelate în seama compilatorului. Exemplul anterior ar putea fi re-scris astfel:float suma(float *tf, int n_e){

float s=0;for (int i=0; i<n_e; i++)

s+=tf[i];return s;

}

int suma(int *ti, int n_e){

int s=0;for (int i=0; i<n_e; i++)

s+=ti[i];return s;

}

Crearea unor funcţii ce au acelaşi nume va purta denumirea de supraîncărcare a funcţiilor. Pentru a putea fi supraîncărcate, funcţiile trebuie să satisfacă următoarele restricţii:

- să aibă număr diferit de parametri fictivi;- dacă numărul de parametri fictivi este acelaşi, tipul parametrilor fictivi trebuie să difere măcar

într-o singură poziţie;- tipul returnat de funcţie nu se ia în consideraţie în procesul de supraîncărcare.Compilatorul de fapt nu lucrează cu numele fizice ale funcţiilor, ci creează nişte denumiri noi,

numite signaturi ale funcţiilor. De exemplu, funcţiileint produs(int i, int j){...}

şifloat produs(float i, float j){...}

ar putea avea respectiv următoarele signaturi: produs_ii

şiprodus_ff

Din câte se vede, signaturile funcţiilor conţin informaţie şi despre tipul parametrilor fictivi, ceea ce este foarte util la determinarea variantei corecte a funcţiei. Deci funcţiile cu acelaşi nume, adică supraîncărcate, au totuşi signaturi diferite. Doar aşa este posibilă o apelare a variantei corecte pentru combinaţia corespunzătoare de parametri reali. Varianta corectă a funcţiei este determinată din confruntarea tipurilor parametrilor fictivi cu tipurile parametrilor reali. În fragmentul ce urmează sunt apelate funcţiile cu nume suma(), definite anterior.void main(){

float tf[5]={1.2,4,3,3.4,0};

32

Page 33: POO - Curs Doc

int ti[3]={1,2,7};cout<<suma(tf,5)<<" "<<suma(ti,3);

}

Desigur că pentru tabloul tf va fi apelată prima funcţie, iar pentru tabloul ti va fi apelată funcţia a doua.

În limbajele care admit supraîncărcarea funcţiilor, un nume de funcţie poate fi legat cu o mulţime de algoritmi posibili (poate fi supraîncărcată de multe ori). Nu există cerinţe stricte referitor la algoritmii ce ţin de un nume de funcţie şi totuşi este de dorit ca algoritmii realizaţi să fie similari ca idee.

Avantajele supraîncărcării funcţiilor sunt următoarele:- simplitatea de elaborare a algoritmilor, având un mecanism mai simplu de interacţiune cu

funcţiile;- simplitatea de întreţinere a codului elaborat, dat fiind o înţelegere mai simplă a ideilor codificate.Mecanismul de supraîncărcare a funcţiilor este şi un generator posibil de erori, care de regulă sunt

numite ambiguităţi. Cel mai simplu este a descrie posibilele ambiguităţi, prezentând un exemplu. Fie funcţia f1(), definită cum urmează:void f1(float f){. . .}//-----void f1(double d){. . .}

În continuare este prezentat un fragment de cod în care sunt efectuate o serie de apelări ale funcţiei date:int i=7;float f=1.2;double d=-3.12; . . .f1(f);f1(d);f1(2.3);f1(i);

Prima apelare se referă la prima variantă a funcţiei, având parametrul real de tip float. Următoarele două se referă la cea de-a doua variantă, având parametrul real de tip double. În schimb, ultima apelare va genera o ambiguitate, fiindcă nu e clar care variantă trebuie să fie apelată pentru un parametru real de tip int.

Deoarece constructorul unei clase este în ultimă instanţă o funcţie, lui i se poate aplica mecanismul de supraîncărcare şi deci existenţa claselor care au mai mulţi constructori se datorează anume posibilităţii de supraîncărcare. Drept rezultat o clasă poate avea mai mulţi constructori. De asemenea, o clasă poate avea şi metode supraîncărcate.

Un alt mecanism important este posibilitatea fixării de valori implicite (predefinite) ale parametrilor fictivi ai funcţiei – se utilizează atunci, când setul de valori reale transmise la apelarea funcţiei are o serie de poziţii în care pot fi repetări ale valorilor. De exemplu, funcţia afisareData()void afisareData(int zi, int luna, int an){

cout<<zi <<”.” <<luna <<”.” <<an << endl;}

poate fi apelată pentru afişarea unor zile de naştere. Pentru studenţii dintr-o grupă există o mare probabilitate că valorile reale din poziţia ce corespunde anului de naştere vor fi egale. Pentru valorile ce ţin de luna naşterii, şansele de egalitate vor fi mai mici, dar tot există. Fixarea valorilor implicite duce la simplificarea expresiei de apelare a funcţiei.

Procesul de fixare a valorilor implicite este ghidat de o serie de reguli:- fixarea valorilor implicite pentru parametrii fictivi se face parcurgând lista parametrilor fictivi de

la dreapta spre stânga;

33

Page 34: POO - Curs Doc

- parametrii fictivi cu valori implicite formează o mulţime compactă, adică nu există parametri fără valori printre parametrii cu valori.

Fixarea valorilor implicite poate fi făcută în două circumstanţe:- în antet, la definirea funcţiei;- în prototipul funcţiei.

Nu este permisă fixarea valorilor şi în antet, şi în prototip. În schimb dacă în antet nu sunt fixate valori implicite, atunci diferite prototipuri declarate local pot avea diferite seturi de valori implicite. De exemplu, funcţia afisareData()cu valori implicite definite în antet la definirea funcţiei.void afisareData(int zi,int luna=11,int an=2007){

cout <<zi <<”.” <<luna <<”.” <<an << endl;}

Exemplul ce urmează se referă la fixarea valorilor implicite în prototipul funcţiei. În două funcţii diferite sunt declarate prototipurile funcţiei afisareData(), dar cu seturi diferite de valori implicite.void afisare2000(){ void afisareData(int zi, int luna, int an=2000); . . .}

şivoid afisare1990(){ void afisareData(int zi,int luna=1,int an=1990);

. . .}

La apelarea unei funcţii parametrii reali transmişi în funcţie sunt parcurşi de la stânga spre dreapta, legându-se cu parametrul fictiv corespunzător şi transmiţându-i valoarea sa. Valorile implicite ale parametrilor fictivi sunt substituite de valorile parametrilor reali. Dacă parametri reali sunt mai puţini decât parametri fictivi, atunci parametrii fictivi rămaşi ne-legaţi vor avea ca valori valorile implicite, dacă le au. În caz că nu le au, va fi generată eroare din insuficienţă de parametri reali. De exemplu, dacă va fi apelată funcţia afisareData() din funcţia afisare1990()afisareData(7, 10, 2004);afisareData(7, 10);

atunci cea de-a doua apelare va fi echivalentă cu următoarea:afisareData(7, 10, 1990);

unde în calitate de al treilea parametru real este luată valoarea implicită.Mecanismul de supraîncărcare a funcţiilor şi fixarea valorilor parametrilor impliciţi pot să ducă la generare de ambiguităţi. În continuare, prin intermediul exemplului care urmează, va fi prezentată o posibilă ambiguitate. void f(int i1, int i2=0, int i3=0){

. . .}void f(int i){

. . .}void main(){

. . .f(1, 2, 3); //a)f(1, 2); //b)f(1); //c). . .

}

Funcţia f() este supraîncărcată, având două variante: cu trei parametri şi cu un parametru. În funcţia main() sunt trei variante de apel al funcţiei f(). Varianta a) va apela funcţia f() cu trei parametri. Varianta b), de asemenea, va apela funcţia f() cu trei parametri, valoarea parametrului al

34

Page 35: POO - Curs Doc

treilea fiind valoarea implicită. Pe când varianta c) - generează ambiguitate fiindcă ar fi bună de apel şi funcţia f() cu trei parametri, având două valori implicite, dar şi cea cu un singur parametru.

Funcţiile membre ale unei clase, de asemenea, pot avea fixate valori implicite pentru parametrii fictivi. Modul de fixare a valorilor implicite ale parametrilor fictivi pentru funcţiile membre nu se deosebeşte de modul descris pentru funcţiile libere.

Fiindcă constructorii unei clase sunt de asemenea funcţii, rezultă că parametrii fictivi ai constructorilor pot de asemenea avea fixate valori implicite. Un constructor cu parametri, dar care are valori implicite pentru toţi parametrii fictivi, este echivalent cu un constructor fără parametri. Acest enunţ poate fi generalizat în felul următor: constructorul care are valori implicite pentru k parametri fictiv este echivalent cu un constructor cu (n-k) parametri, unde n este numărul de parametri fictivi ai constructorului. Fie clasa cl1 având constructor fără parametri class cl1{

.public:

cl1(); .

};

cl1 ob1;

şi clasa cl2 având constructor cu un parametru, dar cu valoare implicită.class cl2{

.public:

cl2(int i=10);.

};

cl2 ob21(1);cl2 ob22;

Din cele spuse anterior, clasa cl2 poate crea obiecte, utilizând forma cu parametri şi forma fără parametri a constructorului.

§9. Definirea şi utilizarea referinţelor

Toate programele utilizate până în prezent utilizau doar două tipuri de variabile şi anume variabile obişnuite şi variabile de tip adresă sau pointeri. Limbajul C++ mai adaugă la aceste tipuri şi variabile de tip referinţă. O variabilă de tip referinţă este precedată la definire de simbolul ampersend (&). Iată cum este definită o referinţă,tip & nume_var_ref=nume_var;

unde nume_var_ref este un identificator, care reprezintă numele variabilei de tip referinţă, iar nume_var este numele unei variabile, care trebuie să aibă o definire anterioară referinţei. Această definire poate fi generalizată pentru cazul mai multor variabile, după cum urmează:tip &nume_var_ref1=nume_var1, . . ., &nume_var_refk=nume_vark;

Pentru a observa unele proprietăţi ale referinţelor este propus un program în care sunt definite referinţe şi sunt efectuate o serie de operaţii atât asupra variabilelor simple cât şi asupra variabilelor de tip referinţă.#include <iostream.h>void main(){

int i=10, j=20;int &ri=i, &rj=j;cout <<i<<’ ’<<ri<<endl;

35

Page 36: POO - Curs Doc

i=j;cout <<i<<’ ’<<ri<<endl;ri+=rj;cout <<i<<’ ’<<ri<<endl;

}

Programul anterior va produce următoarele afişări:10 1020 2040 40

Se observă că orice transformare efectuată asupra variabilei i se răsfrânge asupra referinţei corespunzătoare ri şi de asemenea orice transformare efectuată asupra referinţei ri se răsfrânge asupra variabilei corespunzătoare i. De aici este trasă concluzia că o referinţă este ca un nume nou pentru variabila cu care este legată, adică ar fi ca un alias sau poreclă. Crearea unui nume nou pentru o variabilă nu este un mare avantaj fiindcă ar putea doar să genereze dificultate de a urmări ideea programului şi ar putea fi şi o sursă de erori.

Totuşi referinţele sunt de un real folos în cadrul funcţiilor şi anume în calitate de parametri fictivi şi ca valoare returnată de funcţie. Sunt cunoscute deja metodele de transmitere a parametrilor în funcţie prin valoare şi prin adresă. La aceste metode se mai alătură şi metoda de transmitere a parametrilor prin referinţă. Pentru o comparare a acestor metode vor fi realizate trei funcţii menite să schimbe cu locurile valorile aflate în două variabile.

1) Transmitere prin valoarevoid schimb1( int a, int b){ int t=a; a=b; b=t;}

2) Transmitere prin adresăvoid schimb2( int*a, int*b){ int t=*a; *a=*b; *b=t;}

3) Transmitere prin referinţăvoid schimb3( int&a, int&b){ int t=a; a=b; b=t;}

Aceste funcţii vor fi apelate în funcţia main(), cu scopul realizării operaţiei de schimb a valorilor variabilelor.

#include<iostream.h>void main(){

int a=30, b=60;int &ra=a, &rb=b;schimb1(a,b);cout<<a<<’ ’<<b<<endl;schimb2(&a,&b);cout<<a<<’ ’<<b<<endl;schimb3(ra,rb);cout<<a<<’ ’<<b<<endl;

}

Programul dat va produce următoarele afişări:30 6060 3030 60

Urmărind afişările produse se poate trage concluzia că prima funcţie face un schimb local, care nu afectează nicicum variabilele a şi b, iar funcţiile a doua şi a treia realizează un schimb care afectează valorile variabilelor a şi b, precum şi era necesar. Deci transmiterea parametrilor prin adresă şi prin referinţă sunt asemănătoare prin faptul că schimbarea valorii în cadrul funcţiei a unui parametru din lista parametrilor fictivi se produce neapărat şi asupra parametrului real transmis în funcţie în locul acestui parametru fictiv. Aşadar parametrii fictivi de tip referinţă sunt asemănători cu parametrii de tip pointer. Adică referinţele sunt un fel de adrese.

Pe de altă parte comparând funcţiile se poate observa că modul de prelucrare a referinţelor în cadrul funcţiei este asemănător cu modul de prelucrare a variabilelor obişnuite, adică nu sunt necesare careva

36

Page 37: POO - Curs Doc

simboluri adăugătoare ataşate variabilelor ca în cazul pointerilor (*). Deci parametrii de tip referinţă sunt asemănători atât cu parametrii de tip adresă cât şi cu parametrii de tip valoare.

În programul anterior funcţia schimb3() a fost apelată utilizând variabilele de tip referinţă ra şi rb, corespunzătoare variabilelor a şi b: schimb3(ra,rb);

Poate fi realizat un apel echivalent al funcţiei, utilizând direct variabilele a şi b după cum urmează. schimb3(a,b);

Un exemplu interesant de funcţie este o funcţie care are un parametru fictiv de tip referinţă şi returnează o valoare de tip referinţă. Fie următorul tip de funcţie: int patrat(int i){

i=i*i;return i;

}

Această funcţie se comportă bine în partea dreaptă a unei egalităţi returnând pătratul unui număr. De exemplu:int k=12;j=patrat(k);

Plasarea unei astfel de funcţii în partea stângă a egalităţii ar genera o eroare. patrat(k)=j; //eroare

În continuare funcţia anterioară este schimbată puţin, utilizând referinţe, şi este obţinută următoarea funcţie:int& patrat(int&i){

i=i*i;return i;

}

Plasată în partea dreaptă a egalităţii ea de asemenea returnează pătratul unei valori, insă ea capătă proprietatea neaşteptată de a putea fi plasată şi în partea stângă a egalităţii:j=25;patrat(k)=j; //corect

Drept rezultat valoarea 25 din variabila j va fi plasată în variabila k. Astfel de proprietate poate fi de un real folos în procesul de supraîncărcare a unor operatori.

Există o serie de restricţii de utilizare a referinţelor: 1) Referinţa trebuie iniţializată la definire şi rămâne neschimbată pe parcursul execuţiei programului;2) Nu poate fi determinată adresa unei referinţe;3) Referinţele pot fi comparate, dar nu la nivel de adrese, ci la nivel de valori asociate cu ele.

În continuare va fi menţionată o posibilă eroare legată de referinţe şi care poartă denumirea de pierdere a referinţei. O astfel de eroare se produce atunci când tipul referinţei nu coincide cu tipul variabilei cu care este asociată referinţa: tip1 var;tip2 &r=var;tip1≠tip2;

În baza ideii anterioare este propus următorul exemplu: int i=10;int &ri=i;long j=10;int &rj=j;cout <<i<<’ ’<<ri<<endl;cout <<j<<’ ’<<rj<<endl;i++;j++;cout <<i<<’ ’<<ri<<endl;cout <<j<<’ ’<<rj<<endl;

Fragmentul dat va produce următoarele afişări:

37

Page 38: POO - Curs Doc

10 1010 1011 1111 10

De aici se observă că variabilele i şi ri, fiind definite corect, sunt asociate şi se schimbă sincron, pe când valorile variabilelor j şi rj arată că ele de fapt nu sunt asociate, deci referinţa rj nu este legată corect de variabila j.

§10. Tablouri de obiecte. Pointeri şi referinţe la obiecte. Pointeri la membrii clasei

Deoarece clasele sunt tipuri de date abstracte, în baza lor pot fi create tablouri de obiecte într-un mod similar cu crearea tablourilor în baza unor tipuri predefinite. Pentru tipuri predefinite există două descrieri care permit creare de tablouri, şi anume, fără iniţializarea elementelor:tip_pr nume_tab[dim];

şi cu iniţializarea elementelor:tip_pr nume_tab[dim]={val1, val2, ..., valk};

Cu toate că descrierile pentru tablouri de obiecte sunt asemănătoare, totuşi pentru cazul claselor, trebuie aduse unele concretizări. Vor fi distinse trei posibilităţi, care corespund tipului constructorului din clasă.

a) Dacă clasa are fie constructor implicit, fie constructor fără parametri, fie constructorul poate fi redus la un constructor fără parametri, atunci pentru crearea unui tablou de obiecte va fi utilizată o descriere echivalentă cu descrierea fără iniţializare: nume_cl nume_to[dim];

unde nume_cl este numele clasei în baza căreia este creat tabloul de obiecte, nume_to reprezintă denumirea tabloului de obiecte şi dim este dimensiunea tabloului.

b) Dacă clasa are un constructor cu un parametru sau constructorul poate fi redus la un constructor cu un parametru, atunci pentru crearea unui tablou de obiecte va fi utilizată o descriere echivalentă cu descrierea cu iniţializare: nume_cl nume_to[dim]={val1, val2, ..., valk};

unde val1, val2, …, valk reprezintă valorile în baza cărora lucrează constructorul, creând obiectele. c) Dacă însă constructorul are mai mult de un parametru fictiv, se obţine cazul general de descriere a

tabloului de obiectenume_cl nume_to[N]={nume_cl(pr11,…,pr1k),…, nume_cl(prN1,…,prNk)};

De exemplu, dacă considerăm că clasa carte are constructorul carte(char *autor, char *titlu, int an);

atunci un tablou cu cinci obiecte va fi definit astfelcarte colectie[5]={ carte(”H.Schildt”,”C++ manual complet”,1997), carte(”L.Negresu”,”Limbajele C şi C++”,2000), carte(”B.Stroustrup”,”The C++ Programming Language”,1997), carte(”K.Jamsa”,”Succes cu C++”,1997), carte(”D.Somnea”,”Initiere in C++”,2006)};

Cazul constructorului cu un singur parametru, de asemenea, se încadrează în cazul general, scriind astfel:nume_cl nume_to[dim]={nume_cl(val1),nume_cl(val2) ,…,nume_cl(valk)};

dar desigur că această variantă este mai greoaie ca varianta b). Utilizarea denumirii constructorului pentru descrierea fiecărui obiect generează o cale anevoioasă de

creare, mai ales, când este necesară crearea unui tablou cu un număr mare de obiecte. Când se intenţionează a lucra cu tablouri mari de obiecte, atunci, de regulă, clasa în baza căreia se creează tablourile va trebui să conţină şi un constructor fără parametri sau unul care se reduce la un constructor fără parametri, pentru a evita descrieri pentru fiecare element al tabloului. De exemplu, considerând că clasa punct are un constructor cu valori implicitepunct(int x=0, int y=0);

38

Page 39: POO - Curs Doc

este posibil de a defini un tablou cu o sută de elemente utilizând următoarea descriere: punct puncte[100];

Accesarea unui element al tabloului de obiecte se realizează utilizând un indice întreg nume_to[ind]

iar pentru a accesa un membru al unui element al tabloului de obiecte este utilizată următoarea expresienume_to[ind].nume_membru

indicele ind satisfăcând condiţia0 ≤ ind ≤ dim-1

unde dim este dimensiunea tabloului de obiecte.Deoarece obiectele sunt variabile create în baza unor clase, există posibilitatea de a accesa nu doar

conţinutul lor, ci şi adresele lor. Operarea cu adresele obiectelor se face prin intermediul pointerilor la obiecte. Un pointer la obiecte de un anumit tip este definit astfel:nume_cl *nume_var_po;

unde nume_cl determină tipul obiectelor cu care poate opera pointerul dat. Operatorii admisibili în contextul pointerilor sunt admisibili şi în contextul pointerilor la obiecte.

1) operatorul = care permite plasarea unei adrese într-un pointernume_var_po = expresie;

unde valoarea expresiei din dreaptă a operatorului de atribuire = reprezintă o adresă. Adeseori această expresie este exprimată prin adresa unui obiect, utilizând operatorul & nume_var_po = &nume_ob;

sau prin adresa de început al unui tablou de obiectenume_var_po = nume_to;

sau prin valoare din alt pointernume_var_po = nume_var_po1;

2) având într-un pointer adresa unui obiect, poate fi realizat accesul la un membru al obiectului prin operatorul ->nume_var_po -> nume_membru

3) având într-un pointer adresa unui obiect, poate fi citit conţinutul obiectului prin operatorul *nume_ob = *nume_var_po;

4) dacă într-un pointer este adresa unui obiect, atunci incrementând valoarea dată cu operatorul ++ în pointer se va obţine adresa obiectului următor:nume_var_po++;

5) dacă într-un pointer este adresa unui obiect, atunci decrementând valoarea dată cu operatorul -- în pointer se va obţine adresa obiectului anterior:nume_var_po--;

6) dacă într-un pointer este adresa unui obiect, atunci incrementând valoarea dată cu o valoare naturală n, utilizând operatorul += în pointer se va obţine adresa obiectului al n-lea următor:nume_var_po+=n;

7) dacă într-un pointer este adresa unui obiect, atunci decrementând valoare dată cu o valoare naturală n, utilizând operatorul -= în pointer se va obţine adresa obiectului al n-lea anterior:nume_var_po-=n;

Operatorii aritmetici anteriori, fiind aplicaţi unui pointer, schimbă valoarea din pointer. Pot fi aplicaţi operatorii + şi - fără a schimba valoarea din pointernume_var_po + n

saunume_var_po - n

obţinând adresa obiectului al n-lea următor sau anterior.În contextul pointerilor la obiecte pot fi aplicaţi, de asemenea, şi operatorii relaţionali >, <, >=, <=,

==, !=.La aplicarea operatorului de atribuire are loc controlul riguros al tipului pointerului din partea stângă

a operatorului de atribuire şi tipul expresiei din partea dreaptă. Operaţia de atribuire este efectuată doar la satisfacerea corespondenţei pointerilor. Corespondenţa pointerilor este satisfăcută în următoarele condiţii:

39

Page 40: POO - Curs Doc

I. pointerul din stânga operatorului de atribuire este de acelaşi tip ca şi valoarea din partea dreaptă;

II. dacă pointerul din stânga şi valoarea din dreapta sunt de tipuri diferite, atunci sunt admise doar tipuri “inrudite”, adică clasă de bază – clasă derivată. Tipul din stânga operatorului de atribuire este de tip clasă de bază, iar tipul din dreapta este de tip clasă derivată.

De exemplu, dacă se presupune existenţa unor obiecte şi pointeri de tip clasă de bază şi de tip clasă derivată:cl_baza *pob, ob;cl_d *pod, od;

atunci vor fi valabile următoarele atribuiri:pob = &ob; // corectpod = &od; // corectpob = pod; // corect

şi vor fi inadmisibile următoarele:pod = pob; // incorectpod = &ob; // incorect

În contextul claselor şi al obiectelor, există un pointer predefinit numit this, care indică adresa obiectului curent. Obiectul curent este obiectul care efectuează operaţia de apel a unei funcţii. La implementarea unei clase în orice funcţie membră a clasei, care nu este statică, poate fi utilizat pointerul this. Funcţiile statice nu se bucură de această proprietate. De exemplu, constructorul clasei punct poate fi realizat în felul următor:punct(int x=0, int y=0){this->x=x;this->y=y;}

Specificul acestui constructor constă în faptul că numele parametrilor fictivi x şi y coincid cu numele membrilor clasei x şi y. Pentru a face delimitare între parametrii fictivi şi membrii clasei, numele membrilor clasei sunt precedate de pointerul this conectat prin operatorul ->.

Orice membru al clasei curente poate fi accesat prin intermediul pointerului this, precedând numele membrului cu pointerul this. Dacă nu sunt confuzii de nume, ca în cazul constructorului precedent, pointerul this poate fi omis fără careva pierderi de sens.

Totuşi, sunt situaţii când nu poate fi ocolită utilizarea pointerului this. De exemplu, pointerul this va fi utilizat dacă funcţia membru returnează adresa obiectului curent, cum este arătat în descrierea ce urmează:nume_cl *nume_fm(lista_pf){. . .return this;}

Va fi o situaţie similară dacă funcţia membru returnează obiectul curent, ceea ce este reprezentat în următoarea descriere:nume_cl nume_fm(lista_pf){. . .return *this;}

Dacă funcţia membru utilizează obiectul curent pentru anumite procesări, va fi, de asemenea, necesară utilizarea pointerului this:tip_r nume_fm(lista_pf){. . .nume_cl ob = *this;. . .

40

Page 41: POO - Curs Doc

}

În procesul elaborării de algoritmi pot fi utilizate şi referinţe la obiecte. Definirea unei referinţe de tip obiect poate fi realizată astfel:nume_cl &nume_vr = nume_ob;

unde nume_vr este numele variabilei de tip referinţă, iar nume_ob este denumirea obiectului în baza căruia este definită referinţa.

Referinţele la obiecte se folosesc des anume în calitate de parametri fictivi ai unor funcţii şi în calitate de valoare returnată a funcţiei. Referinţele la obiecte ne dau eficienţă din punct de vedere a utilizării memoriei. Referinţele se utilizează des în constructorii de copiere, în procesul de supraîncărcare a operatorilor, mai ales a celor de intrare/ieşire.

O noţiune nouă în contextul claselor sunt pointerii la membrii clasei. Ei nu sunt nişte adrese, ci mai degrabă nişte deplasamente în raport cu originea clasei. Fiindcă membrii clasei sunt de două tipuri, adică date membre şi funcţii membre, de două tipuri vor fi şi pointerii la membrii clasei.

a) Pointeri la date membre.Un pointer la date membre poate fi definit utilizând următoarea descriere:

tip nume_cl::*nume_pmd;

unde nume_pmd este identificatorul care reprezintă numele pointerului, nume_cl este numele clasei a cărui membru va fi legat cu pointerul, iar tip descrie tipul pointerului. Tipul pointerului coincide cu tipul membrului cu care va fi legat. A defini un pointer nu înseamnă a-l şi iniţializa. Pentru iniţializare este utilizat operatorul & în felul următor:nume_pmd=&nume_cl::nume_md;

Pentru a accesa un câmp al unui obiect folosind pointer la membrul clasei este utilizat operatorul .*nume_ob.*nume_pmd

unde nume_ob este obiectul al cărui membru este accesat, iar nume_pmd este pointer la membrul dat al clasei.

Dacă este utilizat nu numele unui obiect, ci un pointer la obiect, atunci operatorul .* va fi înlocuit cu operatorul ->* după cum urmează:nume_po->*nume_pmd

Pentru a exemplifica utilizarea pointerilor la membrii clasei de tip date, va fi rescrisă clasa punct, declarând toţi membrii publici:class punct { public: int x; int y; punct(int x=0, int y=0); void afisare(); void stingere(); void miscare(int xn, int yn); };

Acum poate fi definit un pointer la datele membre x şi y ai clasei punct:int punct::*pxy; //definire pointer la membrupxy=&punct::x; //initializarepunct p(100,200),*pp; //definire obiect, pointerpp = &p;p.*pxy=150; //schimbare valoarea din 100 in 150pxy=&punct::y; //schimbare valoare pointer pp->*pxy=150; //schimbare valoarea din 200 in 150

b) Pointeri la funcţii membre.Întrucât descrierea unei funcţii constă din mai multe elemente, tot aşa şi definirea pointerilor la funcţii membre va fi diferită de cazul datelor membre, realizându-se astfel:tip (nume_cl::*nume_pmf)([tip1 pf1[, tip2 pf2 [, …,tipk pfk]...]]);

41

Page 42: POO - Curs Doc

unde nume_pmf este numele pointerului la funcţii membre, după el urmând descrierea parametrilor fictivi.

Iniţializarea pointerului la funcţii membre este similară cu iniţializarea pointerului la date membre:nume_pmf=&nume_cl::nume_mf;

unde nume_mf este numele funcţiei membre.Apelarea funcţiei membre prin intermediul pointerului la funcţia membră este realizată prin

intermediul operatorului .* astfel(nume_ob.*nume_pmf)(pr1, pr2, …,prk)

Exemplificarea va fi făcută tot în baza clasei punct.void (punct::*pas)();pas=&punct::afisare;punct p(100,200);(p.*pas)(); //apelarea functiei afisarepas=&punct::stingere;punct p(100,200);(pas.*pas)(); //apelarea functiei stingere

Dacă mai multe funcţii membre au acelaşi prototip, atunci putem avea un pointer de tip „pointer la membrul clasei” care poate fi folosit cu toate funcţiile respective.

§11. Dirijarea dinamică a memoriei

Toate posibilităţile de dirijare dinamică a memoriei existente în limbajul C sunt valabile şi în limbajul C++. Este vorba de funcţiile malloc(), calloc(), realloc(), free(). Iată prototipurile acestor funcţii:void *malloc(size_t dim_loc);void *calloc(size_t num_el, size_t dim_el);void *realloc(void *adr_loc, size_t dim_nou);void free(void *adr_loc);

Limbajul C++ mai are şi posibilităţi proprii încorporate în limbaj sub forma de operatori care permit, de asemenea, dirijarea dinamică a memoriei.

Pentru alocare de memorie este utilizat operatorul new. Modul de utilizare a operatorului new poate fi descris astfel:tip *adr_locatie;. . .adr_locatie=new tip; //aloca memorie de tipul dat

unde adr_locatie este un identificator care reprezintă numele pointerului în care este plasată adresa locaţiei de memorie alocată, utilizând operatorul new, iar tip determină la ce tip de date va fi atribuită locaţia dată de memorie. Dacă operatorul new se execută cu succes, valoarea din pointerul adr_locatie va fi adresa locaţiei de memorie alocată, în caz contrar, în pointer va fi plasată o valoare nulă, care poate fi exprimată prin constanta simbolică NULL. Iată un exemplu:int *adri;adri=new int;

În procesul alocării de memorie în locaţia dată poate fi plasată şi o valoare iniţială. Modul de aplicare a operatorului new se schimbă puţin după cum urmează:tip *adr_locatie;. . .adr_locatie=new tip(val); // alocare cu initializare

De exemplu:float *Pi;Pi = new float(3.14);

ceea ce este echivalent cu:float *Pi;

42

Page 43: POO - Curs Doc

Pi= new float;*Pi = 3.14;

Ideile expuse anterior sunt aplicabile integral tipurilor predefinite de date. În cazul tipurilor abstracte de date (clase) definite de programator trebuie de ţinut cont că la crearea oricărui obiect participă şi constructorul clasei. De aceea în continuare este propusă o descriere care generalizează descrierile anterioare, obţinând o descriere aplicabilă în contextul claselor:nume_cl *adr_obiect;adr_obiect = new nume_cl[(lista_par_reali)];

unde lista_par_reali reprezintă parametrii reali transmişi constructorului pentru a putea îndeplini acţiunile de creare a obiectului şi din câte se vede este un element opţional. Lista parametrilor reali poate lipsi atunci când clasa nu are constructor, sau are constructor fără parametri, sau constructorul poate fi redus la un constructor fără parametri. De exemplu, luând ca bază clasa punct, poate fi adus următorul exemplu:punct *pp;pp=new punct(112,37); //adresa obiect in pointer pppp->afisare();

Pentru a elibera spaţiul alocat pentru o variabilă de anumit tip, este utilizat operatorul delete. Forma de aplicare a operatorului este următoarea: delete adr_locatie;

unde variabila adr_locatie conţine adresa locaţiei de memorie alocată cu ajutorul operatorului new. Dacă este eliberat spaţiul alocat pentru un obiect, va fi apelat mai întâi destructorul clasei respective. Pentru a elibera memoria alocată, în exemplul referitor la clasa punct va fi scris astfel:delete pp;

În afară de posibilitatea de alocare a spaţiului pentru o singură variabilă de anumit tip, există posibilitatea de a aloca spaţiu de memorie pentru un tablou de elemente de anumit tip. În acest scop, va fi utilizată următoarea descriere:tip *adr_locatie;. . .adr_locatie = new tip[dim]; //alocare memorie tablou

unde adr_locatie va conţine adresa spaţiului alocat tabloului de elemente. Ca exemplu va fi alocat spaţiu pentru 10 elemente de tip double:double *adrd;adrd = new double[10];

Vom menţiona faptul că la alocarea de memorie pentru tablouri nu poate fi efectuată concomitent şi iniţializarea elementelor, ca în cazul unei singure variabile. De acest fapt trebuie de ţinut cont mai ales la crearea tablourilor de obiecte, fiindcă crearea unui obiect apelând un constructor cu un anumit set de parametri reali ar însemna o “iniţializare” a obiectului. Şi fiindcă la crearea tablourilor nu este posibilă apelarea unui constructor cu parametri, clasa respectivă neapărat va trebui să conţină un constructor fără parametri sau un constructor care se reduce la un constructor fără parametri. Pentru clasa punct este posibilă crearea unui tablou de 20 obiecte:punct *tabp;tabp = new punct[20];

Eliberarea spaţiului de memorie ocupat de un tablou de elemente se realizează cu ajutorul operatorului delete[] scriind astfel:delete[] adr_locatie;

unde adr_locatie conţine adresa tabloului de elemente.

În cazul tipurilor de date simple, rezultatul acţiunii operatorului delete şi al operatorului delete[] poate fi acelaşi, însă în cazul tablourilor de obiecte sunt cazuri când rezultatul acţiunilor lor

43

Page 44: POO - Curs Doc

nu sunt echivalente. La execuţia operatorului delete[] pentru un tablou de obiecte va fi apelat destructorul pentru fiecare element al tabloului, ceea ce va genera o eliberare corectă a memoriei ocupate. La execuţia operatorului delete pentru un tablou de obiecte va fi apelat destructorul doar pentru primul element al tabloului şi deci poate fi generată o eliberare incompletă a resurselor folosite de obiecte.

Operatorii new şi delete au o serie de avantaje în comparaţie cu funcţiile de dirijare dinamică a memoriei:

I. operatorii new şi delete pot fi supraîncărcaţi schimbând acţiunea lor în cadrul unor clase;

II. în procesul de alocare a memoriei nu sunt necesare anumite conversii ale tipurilor de date;III. nu sunt necesare anumite calcule preliminare pentru a determina dimensiunea fragmentului

de memorie.Iată cum este alocată memoria pentru un tablou de 10 elemente de tip int cu ajutorul funcţiei

malloc()int *ai;ai = (int *)malloc(sizeof(int)*10);

În continuare, pentru comparaţie, este efectuat acelaşi lucru utilizând operatorul new:int *ai;ai = new int[10];

Vom remarca faptul că dacă se folosesc funcţiile malloc(), calloc(), realloc() pentru alocare de memorie, atunci se utilizează doar funcţia free() pentru eliberarea memoriei, iar dacă este folosit operatorul new pentru alocare, atunci eliberarea memoriei se face doar cu operatorul delete. Nu se permite combinarea de diferite mecanisme la nivel de alocare şi eliberare a memoriei.

§12. Constructor de copiere

Orice clasă chiar dacă nu are un constructor de copiere definit explicit, are unul care este un constructor de copiere creat implicit de către compilator. Constructorul de copiere implicit face copierea binar cu binar a informaţiei dintr-un obiect creând un obiect nou. Dar un astfel de mecanism de obţinere a obiectelor-copii poate duce la erori grave. Acest fapt se poate întâmpla când clasa, în baza căreia sunt create obiecte, conţine pointeri care punctează la careva fragmente de memorie alocate dinamic. Dacă vor fi create copii ale unui obiect de acest tip prin metoda constructorului de copiere implicit, atunci vor exista mai multe obiecte care au asociate acelaşi fragment de memorie:

pointer

ob2

Memorie dinamică asociată obiectului pointer

ob1

44

Page 45: POO - Curs Doc

Dacă unul dintre aceste obiecte va fi nimicit, fragmentul de memorie asociat va fi eliberat şi atunci obiectele rămase vor arăta la un fragment de memorie inexistent. Dacă vor fi efectuate operaţii în cadrul acestui fragment de memorie, ele vor genera erori.

Constructorul de copiere este apelat în următoarele situaţii posibile:1. Definirea unei variabile de tip obiect, iniţializând-o în baza unui obiect existent;2. Transmiterea de obiecte în calitate de parametri reali ai unei funcţii care are parametri

fictivi de tip obiect. Fiecare obiect va genera o copie, creată cu ajutorul constructorului de copiere.3. Funcţiile care returnează o valoare de tip obiect în momentul returnării valorii creează o

copie a obiectului.Erori de tipul celei descrise anterior pot fi obţinute uşor, de exemplu, la transmiterea unui obiect în

calitate de parametru real la apelarea unei funcţii. Va fi creat un obiect-copie, care la terminarea funcţiei va fi nimicit. Astfel, obiectul transmis în calitate de parametru real va fi afectat serios.

Pentru a evita asemenea erori trebuie de definit în cadrul clasei un constructor de copiere, care ar schimba scenariul de lucru al constructorului de copiere implicit. Şi anume, obiectul-copie ar trebui să-şi aibă propria memorie asociată, care conţine informaţie similară cu memoria asociată obiectului, în baza căruia este creată copia:

Un constructor de copiere are un parametru fictiv de tip referinţă şi este descris în felul următor:class nume_cl{. . .nume_cl(nume_cl &ob);. . .};

iar implementarea lui poate fi descrisă astfel:nume_cl::nume_cl(nume_cl &ob){// instructiuni}

Exemplu:class tablou{

int *date;int dim;

public:tablou(int d, int val);tablou(tablou &t);~tablou();void modificaTablou();void afisare();

};//-----------------

Memorie dinamică asociată obiectului

pointer

ob2

Memorie dinamică asociată obiectului pointer

ob1

45

Page 46: POO - Curs Doc

tablou::talbou(int d, int val){

date=new int[d];if(date!=NULL){ dim=d; for(int i=0;i<dim;i++) date[i]=val;}else dim=0;

}//-----------------tablou::tablou(tablou &t){

dim=t.dim;date=new int[dim];for(int i=0;i<dim;i++) date[i]=t.date[i];

}//-----------------tabou::~tablou(){

delete[] date;}//-----------------void tablou::modificaTablou(){ date[0]=date[0]+11; // mareste primul element cu 11}//-----------------void tablou::afisare(){ for(int i=0;i<dim;i++) cout<<date[i]<<’ ’; cout<<endl;}//-----------------void main(){

tablou t1(15,0), t2(25,1);t1.afisare();t2.afisare();

tablou t3(t2);t3.modificaTablou();t3.afisare();

}

Deoarece, în acest exemplu, este creat un constructor de copiere explicit (corect), orice schimbări asupra obiectului t3 nu afectează obiectul t2 şi invers.

§13. Funcţii prietene şi clase prietene

Clasele sunt proiectate în corespundere cu principiile caracteristice tipurilor abstracte de date. Ele au o serie de proprietăţi, care, de regulă, sunt închise pentru lumea exterioară şi o serie de operaţii care determină funcţionalitatea tipului dat şi care permit şi accesul la proprietăţile tipului dat. Atunci când este necesar un proces rapid de prelucrare, apelarea multor funcţii încetineşte procesul de execuţie şi pentru sistemele de execuţie în timp real nu este efectivă.

Dacă ar fi proiectate clasele vector şi matrice, ar putea fi necesară şi operaţia produs matrice-vector. Dacă nu este logică definirea acestei operaţii ca operaţie internă a uneia dintre clase, atunci ea va fi definită ca o funcţie externă. Schematic această situaţie poate fi reprezentată astfel:class vector{

46

Page 47: POO - Curs Doc

. . .int el_v[100];

int dim;. . .

public:. . .int element_v (int ind);void sc_elem_v (int ind, int v_n);

int dimensiune_v();. . .

};//--------------class matrice{

. . .int el_m[100][100];

int dim;. . .

public:. . .int element_m (int ind1, int ind2);void sc_elem_m (int ind1, int ind2, int v_n);

int dimensiune_m();. . .

};//--------------//functie externa care realizeaza//produsul matrice-vectorvector prod_m_v (matrice m, vector v) { vector vr; int i, j, s;

. . .for (i=0;i<v.dimensiune_v();i++){ s=0; for(j=0;j<v.dimensiune_v();j++) s+=m.element_m(i,j) * v.element_v(j); vr.sc_elem_v(i,s);}. . .return vr;

}

Din definirea funcţiei prod_m_v() se poate vedea că ea conţine foarte multe apeluri de funcţie, ceea ce sigur măreşte timpul de execuţie.

Deoarece în unele situaţii poate fi necesar un acces mai rapid la unele resurse ale clasei, s-a convenit a permite, în unele situaţii, unor funcţii externe acces direct la membrii privaţi sau protejaţi ai clasei date. Astfel de funcţii sunt numite funcţii prietene ale clasei date. Pentru ca o funcţie să fie recunoscută drept funcţie prietenă, ea trebuie să fie declarată prietenă în interiorul clasei. Descrierea este înfăptuită cu ajutorul cuvântului-cheie friend. Nu are importanţă locul de plasare a descrierii. Descrierea corespunzătoare poate fi plasată în câmpul de acţiune a oricărui modificator de acces:class nume_cl { . . . friend tipr nume_func_pr(lista_par_fictivi); . . . };

Definirea funcţiei se face în mod obişnuit ca orice altă funcţie externă clasei. Modificatorul friend nu este plasat în antetul funcţiei la definire:tipr nume_func_pr(lista_par_fictivi){ . . .

47

Page 48: POO - Curs Doc

//instructiuni . . .}

Dacă descrierea friend vector prod_m_v (matrice m, vector v);

va fi plasată în declarările claselor vector şi matrice, atunci funcţia prod_m_v() va deveni prietenă a claselor date. Dacă este declarată drept funcţie prietenă, atunci poate fi simplificat modul ei de realizare obţinându-se acces direct la toţi membrii claselor. După ce o declarăm funcţie prietenă, ea va avea următoarea realizare://functie externa care realizeaza//produsul matrice-vectorvector prod_m_v (matrice m, vector v) {

vector vr; int i, j;

. . .for (i=0; i<v.dimensiune_v(); i++){ vr.el_v[i] = 0;

for (j=0; j<v.dimensiune_v(); j++) vr.el_v[i] += m.el_m[i][j] * v.el_v[j];

}. . .return vr;

}

O funcţie prietenă unei clase poate fi funcţie membră a altei clase. La declararea ei ca funcţie prietenă numele ei este precedat de numele clasei din care face parte:class nume_cl{

. . .friend tipr nume_clasa::nume_fp(lista_pf);. . .

};

Atunci când toate funcţiile membre ale unei clase sunt prietene a altei clase, este obţinută noţiunea de prietenie a claselor, adică prima clasă este prietenă cu cea de-a doua.class B{

. . .// membri

. . .};class A {

. . . friend class B;

. . .};

Relaţia de prietenie în cadrul claselor nu este una comutativă. Adică dacă clasa A este prietenă cu B, aceasta nu înseamnă că clasa B este prietenă cu A.

Tot aşa ea nu este una tranzitivă. Dacă clasa A este prietenă cu B şi clasa B este prietenă cu C, aceasta nu înseamnă că clasa A este prietenă cu C.

§14. Supraîncărcarea operatorilor

Un tip de date este caracterizat nu doar prin domeniul de valori admisibile, dar şi printr-o serie de operaţii care sunt permise asupra lor, numită funcţionalitatea tipului. În cadrul tipurilor predefinite operaţiile sunt exprimate prin operatori, ceea ce nu este valabil pentru tipurile abstracte de date create de

48

Page 49: POO - Curs Doc

programator. În cadrul tipurilor abstracte de date funcţionalitatea este exprimată prin funcţii membre. Însă expresiile scrise în bază de operatori sunt mai simple şi pot fi mai uşor citite, comparativ cu cele scrise în bază de funcţii. De exemplu, expresia:C+A*B

exprimată prin operatori este mult mai simplă decât expresia:C.Adunare(A.Produs(B))

exprimată prin funcţii.Pentru a extinde posibilitatea de utilizare a unui operator în contextul unor obiecte, este necesar de a

realiza procesul de supraîncărcare a operatorului dat în cadrul clasei. O supraîncărcare a unui operator în interiorul unei clase înseamnă a-i da un anumit sens operatorului vizat în cadrul clasei date, păstrându-i sensul sau semnificaţia pentru celelalte tipuri de date.

Supraîncărcarea operatorilor este legată de supraîncărcarea funcţiilor, deoarece procedeul de supraîncărcare a operatorilor se realizează în baza funcţiilor.

Supraîncărcarea operatorilor se face prin următoarele două metode:- supraîncărcarea ca funcţii membre;- supraîncărcarea ca funcţii prietene.

Unii operatori pot fi supraîncărcaţi prin ambele metode, iar alţii pot fi supraîncărcaţi doar printr-o singură metodă. De exemplu, operatorul aditiv + poate fi supraîncărcat prin ambele metode. Pe când operatorul indice [] poate fi supraîncărcat doar prin metoda funcţiilor membre, iar operatorul de extragere >> poate fi supraîncărcat doar prin metoda funcţiilor prietene.

În procesul de supraîncărcare a operatorilor sunt respectate o serie de restricţii:- supraîncărcarea nu poate schimba aritatea operatorului, adică dacă operatorul este unar sau binar,

atunci după supraîncărcare el va fi respectiv tot unar sau binar;- supraîncărcarea nu poate schimba precedenţa operatorilor;- funcţiile prin intermediul cărora se face supraîncărcarea nu pot avea valori implicite.Pot fi supraîncărcaţi toţi operatorii cu excepţia următorilor patru:- ?: operatorul condiţional ternar;- :: operatorul rezoluţiei;- . operatorul punct de acces la membrii clasei;- .* operatorul de acces la membrii clasei prin intermediul pointerilor la membrii clasei.În continuare va fi caracterizat procedeul de supraîncărcare a operatorilor prin metoda funcţiilor

membre. Particularitatea principală este faptul că funcţia membră, prin intermediul căreia va fi efectuată supraîncărcarea unui operator, va fi o funcţie cu un nume special format prin utilizarea cuvântului-cheie operator. În rest este o funcţie membră obişnuită şi deci va fi descrisă şi definită ca şi oricare altă funcţie membră a clasei:class nume_cl //Ø–orice operator supraincarcabil{

. . .tip_r operator Ø (lista_pf);. . .

}. . .tip_r nume_cl :: operator Ø (lista_pf) {

//instructiuni}. . .

unde Ø este numele operatorului supraîncărcat, iar lista_pf reprezintă lista parametrilor fictivi ai funcţiei. Lista parametrilor fictivi conţine, de regulă, un parametru pentru operatorii binari sau este vidă pentru operatori unari. Acest moment se datorează faptului că orice funcţie membră ne-statică are acces la pointerul intern this. Astfel, pentru un operator binar primul operand este obiectul care apelează funcţia membru, iar al doilea operand este reprezentat de parametrul care substituie parametrul fictiv. Pentru un operator unar operandul este obiectul care apelează funcţia membru. Prin pointerul this există acces anume la obiectul care apelează funcţia membru.

49

Page 50: POO - Curs Doc

Exemplu. De alcătuit un program în care este implementată clasa complex, definind operatorii caracteristici numerelor complexe prin supraîncărcarea operatorilor prin metoda funcţiilor membre.

#include <iostream.h>#include <iomanip.h>

class complex{

float r,i;public:

complex (float r1=0, float i1=0) { r=r1; i=i1; }

complex operator + (complex);complex operator - (complex);complex operator * (complex);complex operator / (complex);void afisare ();

};

complex complex :: operator + (complex c) {

complex s;s.r = r + c.r;s.i = i + c.i; return s;

}

complex complex :: operator - (complex c) {

complex s;s.r = r - c.r;s.i = i - c.i; return s;

}

complex complex :: operator * (complex c){

complex p;p.r = r*c.r – i*c.i;p.i = r*c.i + i*c.r; return p;

}

complex complex :: operator / (complex c){

complex r;r.r = (c.r*r + c.i*i)/(c.r*c.r + c.i*c.i);r.i = (c.r*i – r*c.i)/(c.r*c.r + c.i*c.i);return r ;

}

void complex :: afisare (){

cout << r << setiosflafs(ios :: showpos) << i << "i" << resetiosflags (ios :: showpos); }

void main (){

complex c1(7, -2.3), c2(3, 4.9);complex s, d, i, c;s = c1. operator + (c2);

50

Page 51: POO - Curs Doc

s = c1 + c2;d = c1 - c2;i = c1 * c2;c = c1 / c2;s.afisare (); d.afisare (); i.afisare (); c.afisare ();

}

§15. Supraîncărcarea operatorilor prin funcţii prietene

Supraîncărcarea prin intermediul funcţiilor prietene este necesară în unele situaţii pentru eficienţă, iar în unele situaţii concrete este unica metodă de supraîncărcare a operatorilor. Considerăm următoarea problemă: în clasa complex este supraîncărcat operatorul + ce oferă posibilitatea de a aduna un număr complex cu un număr real cu virgulă mobilă, rezultatul fiind, desigur, un număr complex. Supraîncărcarea este efectuată prin metoda funcţiilor membre. O descriere schematică a acestei situaţii urmează în continuare:class complex{. . .complex operator + (float f) { . . . }. . .};. . .complex c1(5,7), r; // c1+ffloat f=2.7 ; . . .r= c1+f;

Utilizarea operatorului + pentru calcularea lui c1+f este corectă, dar apare următoarea întrebare: e posibil oare a utiliza o expresie de felul f+c1? Din punct de vedere matematic sunt expresii echivalente, dar din punctul de vedere al programării, va fi generată o eroare din motiv că variabila f este de tip real cu virgulă mobilă şi nu are definită adunarea cu un număr complex. Deci, trebuie de definit o funcţie care ar putea calcula expresiile de tipul f+c1. Dar supraîncărcarea directă a operatorului + prin metoda funcţiilor membre este imposibilă. Astfel, se va recurge la aplicarea metodei de supraîncărcare prin metoda funcţiilor prietene. În asemenea situaţii, se obţine mai multă eficienţă, putând utiliza în expresii proprietatea de comutativitate.

A fost menţionat deja faptul că unii operatori pot fi supraîncărcaţi doar prin metoda funcţiilor prietene. Ca exemplu pot fi aduşi operatorii de inserţie şi extragere (intrarea/ieşirea informaţiei) care acceptă supraîncărcarea doar prin metoda funcţiilor prietene.

Pentru a realiza supraîncărcarea unui operator prin metoda funcţiilor prietene, se realizează următoarea schemă generală:class nume_cl{

. . .friend tip_r operator Ø (lista_pf);. . .

};tip_r operator Ø (lista_pf){

. . .}

51

Page 52: POO - Curs Doc

unde Ø este numele operatorului supraîncărcat, iar lista_pf reprezintă lista parametrilor fictivi ai funcţiei. Fiindcă o funcţie prietenă nu are acces la pointerul this, lista parametrilor fictivi conţine de regulă doi parametri pentru operatorii binari sau un parametru fictiv pentru operatori unari.

Exemplu. De alcătuit un program în care este implementată clasa complex, definind operatorii caracteristici numerelor complexe prin supraîncărcarea operatorilor prin metoda funcţiilor prietene.#include <iostream.h>#include <iomanip.h>

class complex{

float r, i;public: complex (float r1=0, float i1=0) { r=r1; i=i1; } friend complex operator +(complex a,complex b); friend complex operator *(complex a,complex b); friend complex operator +=(complex &a,complex b); friend complex operator +(complex c,float f); friend complex operator +(float f,complex c); void afisare ();};//------------------complex operator + (complex a, complex b){

complex c;c.r = a.r + b.r ;c.i = a.i + b.i ;return c ;

}//------------------complex operator + (complex a, float f){

complex c;c.r = a.r + f;c.i = a.i;return c;

}//------------------complex operator + (float f, complex a){

complex c;c = a + f; return c;

}//------------------complex operator += (complex &a, complex b){

a.r += b.r ;a.i += b.i;return a;

}//------------------complex operator * (complex a, complex b){

complex p;p.r = a.r * b.r – a.i * b.i;p.i = a.r * b.i + a.i * b.r; return p;

}//------------------void complex :: afisare (){

cout << r << setiosflafs (ios :: showpos)

52

Page 53: POO - Curs Doc

<< i << "i" << resetiosflags (ios :: showpos); }//==================void main (){

complex a(2,4), b(-3, 7.2), s, p;float f1=2.7;s=f1+a;s.afisare();(a*b+f1).afisare();b+=a;b.afisare();

}

§16. Supraîncărcarea unor operatori speciali

Pe lângă operatori tradiţionali, cum ar fi, de exemplu, +, -, *, / pot fi supraîncărcaţi şi o serie de operatori speciali unii având şi anumite particularităţi.

a) operatorii de incrementare şi decrementareFăcând referinţă la operatorii de incrementare şi decrementare, trebuie de ţinut cont că fiecare dintre

ei are două forme: prefix (++i sau --i) şi postfix (i++ sau i--). De aceea, dacă se doreşte o utilizare flexibilă a acestor operatori, este necesar de a supraîncărca atât forma prefix, cât şi forma postfix. Aceşti operatori pot fi supraîncărcaţi prin ambele metode: prin funcţii membre şi prin funcţii prietene. Deoarece operatorii sunt unari, rezultă că funcţiile care realizează supraîncărcarea fie că nu au parametri fictivi pentru metoda funcţiilor membre, fie au un parametru fictiv pentru metoda funcţiilor prietene. Pentru a obţine supraîncărcare şi pentru cealaltă formă, funcţia care realizează supraîncărcarea va fi dotată cu un parametru formal ne-utilizabil. Deci, vorbind despre supraîncărcarea operatorului de incrementare prin metoda funcţiilor membre, într-o clasă oarecare vor exista descrise prototipurile:class nume_cl{. . .tip_r operator ++ (); // forma prefixtip_r operator ++ (int i); // forma postfix. . .};

Într-un mod similar va fi făcută descrierea pentru operatorul de decrementare, dacă este necesar acest lucru.

Pentru clasa concretă complex, vor fi următoarele prototipuri:class complex{. . .complex operator ++ ();complex operator ++ (int i);. . .};

Prototipurilor acestea le corespund următoarele funcţii membre, care sunt incluse în descrierea clasei complex:

- forma prefix:complex complex :: operator ++(){

r++;return *this;

}

- forma postfix:complex complex :: operator ++(int i){

complex a= *this;r++ ;

53

Page 54: POO - Curs Doc

return a;}

La supraîncărcarea operatorului de incrementare prin metoda funcţiilor prietene, într-o clasă oarecare vor exista descrise prototipurile:class nume_cl{. . .// forma prefix friend tip_r operator ++ (nume_cl &ob);// forma postfix friend tip_r operator ++ (nume_cl &ob, int i);. . .};

Întrucât operatorul de incrementare schimbă obiectul asupra căruia acţionează, parametrii fictivi de tip obiect sunt transmişi prin referinţă, pentru ca transformarea lor în cadrul funcţiei să fie vizibilă şi în exteriorul funcţiei.

Cu referire la clasa complex, pot fi făcute următoarele descrieri:- descrierea prototipurilor:

class complex{. . .friend complex operator ++(complex &c);friend complex operator ++(complex &c, int i);. . .};

- forma prefix:complex operator ++(complex &c){

c.r++;return c;

}

- forma postfix:complex operator ++(complex &c, int i){

complex a = c;c.r++;return a;

}

Supraîncărcarea operatorului de decrementare se realizează într-un mod similar.

b) operatorii de inserţie şi extragereSupraîncărcarea operatorilor de inserţie (<<) şi extragere (>>) în cadrul unei clase permite ca

operaţiile de afişare şi citire a obiectelor să fie efectuate într-un mod similar ca şi a datelor de tipuri predefinite, adică:cout << nume_obiect;

sau, respectiv:cin >> nume_obiect;

Supraîncărcarea operatorilor de inserţie (<<) şi extragere (>>) în cadrul unei clase poate fi realizată doar prin metoda funcţiilor prietene. Fiindcă aceşti operatori inter-acţionează cu fluxurile de intrare şi de ieşire, funcţiile care realizează supraîncărcarea au un parametru fictiv, care reprezintă un obiect de tip flux. Operatorul de inserţie << este supraîncărcat pentru tipurile de date predefinite în clasa ostream, iar operatorul de extragere >> este supraîncărcat pentru tipurile predefinite în clasa istream. De aceea parametrii fictivi care reprezintă obiectul de tip flux vor fi anume de aceste tipuri. Clasa în care are loc supraîncărcarea operatorilor de inserţie şi extragere va conţine următoarele prototipuri de funcţii:class nume_cl{. . .

54

Page 55: POO - Curs Doc

friend ostream & operator <<(ostream &nume_flux, nume_cl ob);friend istream & operator >>(istream &nume_flux, nume_cl &ob);. . .}

Primul parametru fictiv al ambelor funcţii reprezintă fluxul de intrare sau de ieşire, el fiind transmis prin referinţă, fiindcă fluxul în procesul operaţiei de intrare sau de ieşire se schimbă şi această schimbare trebuie să fie vizibilă şi în exteriorul funcţiei. Al doilea parametru fictiv reprezintă obiectul, care va fi afişat sau care va fi iniţializat cu date citite de la dispozitive de intrare. Pentru ca valoarea obiectului schimbat în procesul iniţializării să fie vizibilă în exteriorul funcţiei în care se produce schimbarea, parametrul este transmis prin referinţă. În cazul afişării, obiectul este transmis prin valoare, fiindcă la afişare obiectul nu-şi schimbă valoarea. Ambele funcţii returnează referinţa la fluxul corespunzător prelucrat. Implementarea operatorilor va avea forma:ostream & operator <<(ostream &nume_flux, nume_cl ob){ . . // instructiuni . }

şiistream & operator >> (istream &nume_flux, nume_cl &ob){ . . // instructiuni . }

Vom supraîncărca aceşti operatori pentru clasa complex:class complex{. . .friend ostream & operator << (ostream &fluxIes, complex c);friend istream & operator >> (istream &fluxIn, complex &c);. . .};

ostream & operator << (ostream & fluxIes, complex c){ if (c.i == 0) fluxIes << a.r ;else if (c.r == 0) fluxIes << c.i << "i" ; else fluxIes << c.r << setiosflags(ios::showpos) << c.i << "i" << resetiosflags(ios::showpos);

return fluxIes;}

istream & operator >> (istream &fluxIn, complex &c){

fluxIn >> c.r ;fluxIn >> c.i ;return fluxIn;

}

c) operatorul indiceSupraîncărcarea operatorului indice ([]) este realizată atunci, când se doreşte accesul la datele ce

formează conţinutul unui obiect după modelul accesului la elementele unui tablou, adică:nume_ob[indice]

55

Page 56: POO - Curs Doc

Operatorul indice poate fi supraîncărcat doar prin metoda funcţiilor membre. Acest operator este unul binar, de aceea lista parametrilor fictivi este formată dintr-un singur parametru, şi anume indicele. Trebuie de ţinut cont de faptul că operatorul indice poate fi utilizat în expresii atât în partea stângă, cât şi în partea dreaptă a operatorului de atribuire. Supraîncărcarea operatorului este realizată printr-o funcţie, iar o funcţie poate fi plasată în partea stângă a operatorului de atribuire doar când returnează o valoare de tip referinţă. De aceea funcţia care realizează supraîncărcarea operatorului indice va trebui să returneze o valoare de tip referinţă. Pentru descrierea funcţiei va fi utilizat în caz general următorul prototip:class nume_cl{. . .tipr & operator [](tip indice). . .}

Tipul utilizat la descrierea parametrului indice este deseori tipul int, dar nu sunt excluse în descriere şi alte tipuri de date, obţinând astfel un indice generalizat. Din ideea că tipul parametrului indice poate fi descris prin diferite tipuri de date rezultă că o clasă poate avea mai multe supraîncărcări ale operatorului [].

Exemplu. De alcătuit un program în care este implementată clasa vector, realizând supraîncărcarea operatorului indice pentru accesul la componentele vectorului.#include<iostream.h>#include<conio.h>

class vector{

int dim;float *componente;

public:vector (int d){

componente=new float[d];if(componente) dim=d; else dim=0;

for(int i=0; i<dim; i++) componente[i]=0;}~vector (){

delete [] componente; }

float & operator[] (int ind){

return componente[ind]; }

friend ostream & operator <<(ostream & fluxIes, vector v);};//------ostream & operator <<(ostream &fluxIes, vector v){

fluxIes<<”(”;for(int i=0; i<v.dim-1; i++) fluxIes<<v.componente[i]<<”,”;if(v.dim>=1) fluxIes<<v.componente[i];fluxIes<<”)”;return fluxIes;

}//--------void main(){

vector v1(3), v2(7);v1[0]=3.4;v1[1]=2.1;v1[2]=-3.2;v2[0]=v1[0]+v1[1];v2[2]=v1[0]-5.6;

56

Page 57: POO - Curs Doc

v2[3]=v2[2]+v1[1];cout << v1 << endl << v2 << endl;getch();

}

d) operatorul funcţieSupraîncărcarea operatorului funcţie (()) este realizată atunci, când se doreşte accesul la conţinutul

unui obiect, efectuând anumite prelucrări sau transformări. Accesul la conţinutul obiectului este efectuat după modelul apelării unei funcţii, adică:

nume_ob(lista_parametri_reali)

Operatorul funcţie poate fi supraîncărcat doar prin metoda funcţiilor membre. Acest operator este unul binar, însă lista parametrilor fictivi poate fi formată, de la caz la caz, dintr-un număr diferit de parametri tot aşa precum şi lista de parametri a diferitelor funcţii poate consta din număr diferit de parametri. Din acest fapt rezultă că o clasă poate avea mai multe supraîncărcări ale operatorului (), deosebindu-se după numărul de parametri sau tipurile parametrilor fictivi. În contextul clasei complex s-ar putea realiza două variante de supraîncărcare a operatorului funcţie cu următoarele sensuri:

- una ar calcula modulul numărului complex;- alta ar modifica partea reală şi partea imaginară a numărului complex.

Pentru aceasta în clasa complex vor fi introduse următoarele prototipuri de funcţii:class complex{. . .float operator ()();complex operator ()(float r, float i);. . .};

Funcţiile membre corespunzătoare vor fi implementate după cum urmează: - funcţia de calculare a modulului:

float complex :: operator ()(){

return sqrt(r*r+i*i);}

- funcţia de modificare a părţii reale şi a părţii imaginare a numărului:complex complex :: operator ()(float r, float i){

this->r = r;this->i = i;return *this;

}

O aplicare a acestor operatori poate fi următoarea:. . .void main(){complex c(1.3,-2.5);cout << c() << endl; // modulul numar complexc(3.7,2.8); //schimbare numar

}

e) operatorii new şi deleteOperatorii new şi delete pot fi supraîncărcaţi pentru o anumită clasă, dar pot fi supraîncărcaţi şi

la nivel global. Fiind supraîncărcaţi la nivelul unei clase, i-şi schimbă specificul acţiunii doar în raport cu clasa dată, iar la nivel exterior îşi păstrează modul de executare. La nivel de clasă operatorii daţi pot fi supraîncărcaţi doar prin metoda funcţiilor membre. De asemenea, trebuie de ţinut cont de faptul că operatorii daţi au două forme, una pentru variabile simple new, delete şi alta pentru tablouri new[], delete[]. Pentru a realiza supraîncărcarea, în clasă sunt incluse următoarele prototipuri:class nume_cl

57

Page 58: POO - Curs Doc

{. . .void *operator new (size_t dim);void operator delete (void *adr);void *operator new[] (size_t dim);void operator delete[] (void *adr);. . .}

Cea mai simplă formă de supraîncărcare ar putea fi oferită de următoarele implementări ale funcţiilor:void *nume_cl :: operator new (size_t dim){

return malloc (dim);}

void nume_cl :: operator delete (void *adr){ free (adr);}

void *nume_cl :: operator new[] (size_t dim){

return malloc (dim);}

void nume_cl :: operator delete[] (void *adr){ free (adr);}

(nu prea are sens să supraîncărcăm aceşti doi operatori)

f) operatorul virgulăOperatorul virgulă (,) poate fi supraîncărcat atât prin metoda funcţiilor prietene, cât şi prin metoda

funcţiilor membre. Este un operator binar şi nu prezintă vre-o excepţie de la regulile generale în ceea ce priveşte numărul de parametri fictivi ai funcţiilor ce realizează supraîncărcarea: deci un parametru fictiv la supraîncărcare prin metoda funcţiilor membre şi doi parametri la supraîncărcare prin metoda funcţiilor prietene. Pentru a exemplifica, se va face referire la clasa complex. În această clasă, vor fi introduse următoarele prototipuri de funcţii:class complex{. . .complex operator ,(complex c);friend complex operator ,(complex c, float f);. . .};

În continuare, urmează o implementare a acestor funcţii:- funcţia membru

complex complex :: operator , (complex c){return c;}

- funcţia prietencomplex operator , (complex c, float f){complex cf(f,0);return cf;}

58

Page 59: POO - Curs Doc

Ambele funcţii realizează logica operatorului virgulă, adică valoarea oferită de operator este valoarea celui de al doilea operand, valoarea primului operand fiind ignorată.

g) operatorul de conversieEste cunoscut faptul că acţiunea de trecere a unor date de un anumit tip la alt tip de date reprezintă

esenţa operatorului de conversie, care este un operator unar. Acest operator, de asemenea, poate fi supus acţiunii de supraîncărcare în cadrul unei clase. Ideea supraîncărcării acestui operator în cadrul clasei date va fi de a transforma obiectele obţinute în baza clasei date la alt tip de date. Supraîncărcarea operatorului de conversie poate fi făcută doar prin metoda funcţiilor membri. Funcţia care realizează supraîncărcarea nu are specificat tipul valorii returnate, cu toate că după logică ea totuşi returnează o valoare. În schimb, numele operatorului este chiar tipul valorii returnate. Deci clasa, care realizează una sau mai multe conversii, va conţine unul sau mai multe prototipuri de următorul fel:class nume_cl{. . .operator tip ();. . .}

Exemplu. De alcătuit un program în care este implementată clasa lungime, realizând supraîncărcarea unui operator de conversie.#include<iostream.h>#include<conio.h>

class lungime{

int metri;int centimetri;

public:lungime (int m, int c){

metri=m;centimetri=c;

}operator float (){

return (metri+centimetri/100.0); }};//==========void main(){

lungime L1(3, 20), L2(7, 86);cout << float(L1) << ’ ’ << float(L2) << endl;getch();

}

Din exemplu se vede, că în cadrul clasei lungime a fost efectuată supraîncărcarea operatorului de conversie la tipul float şi drept rezultat lungimile L1 şi L2 vor fi afişate sub forma 3.2 şi 7.86.

§17. Funcţii-şablon şi clase-şablon

Funcţiile-şablon sunt concepute pentru a uşura procesul de creare a unor funcţii care realizează algoritmi similari, deosebindu-se doar prin tipul datelor prelucrate.

Să considerăm următorul exemplu. Să se realizeze două funcţii: una permite găsirea elementului maximal al unui tablou de numere întregi, iar alta permite găsirea elementului maximal al unui tablou de numere cu virgulă mobilă.

59

Page 60: POO - Curs Doc

Presupunând că tabloul de numere întregi este de tip int, iar tabloul de numere cu virgulă mobilă este de tip float, sunt propuse următoarele funcţii:

- pentru tabloul de numere întregi:

int el_max(int *tb, int n_el){

int max=tb[0];for (int i=1; i< n_el ; i++)

if (max < tb[i]) max=tb[i];return max;

}

- pentru tabloul de numere cu virgulă mobilă:float el_max (float *tb, int n_el){

float max=tb[0];for (int i=1; i< n_el ; i++)

if (max < tb[i]) max=tb[i];return max;

}

Din compararea acestor funcţii se poate observă că ele sunt identice prin setul de instrucţiuni utilizate şi se deosebesc doar prin tipurile unor variabile locale sau ale unor parametri fictivi. Pentru a descrie mai simplu astfel de algoritmi se utilizează funcţiile-şablon. Limbajul C++ are încorporat în el mecanismul funcţiilor-şablon. Ele se mai numesc şi funcţii parametrizate sau funcţii generice. Prin intermediul unei singure descrieri a funcţiei-şablon este realizată, de fapt, descrierea unei mulţimi de funcţii, câte una pentru diferite combinaţii de tipuri de date. O funcţie-şablon este descrisă în felul următor:

template<class Ta1, class Ta2, . . ., class Tak>[tip_r] nume_functie([lista_par_fictivi]){

. . .//instructiune. . .

}

unde template este cuvântul-cheie; Ta1, Ta2, …, Tak sunt o serie de tipuri abstracte de date, iar celelalte elemente ale funcţiei sunt cunoscute din descrierea funcţiilor obişnuite. Tipurile abstracte de date pot fi utilizate ca descriptori pentru descrierea parametrilor fictivi sau a variabilelor locale. Tipul valorii returnate de funcţie poate fi la necesitate descris cu ajutorul unuia dintre tipurile abstracte. Revenind la exemplul precedent, în conformitate cu descrierea generală este obţinută următoarea funcţie-şablon:template<class Tip>Tip el_max (Tip *tb, int n_el){

Tip max;max=tb[0];for (int i=1 ; i < n_el ; i++)

if (max < tb[i]) max =tb[i];return max;

}

Din funcţia-şablon este obţinută funcţia reală necesară. Funcţiile reale se obţin la etapa compilării prin înlocuirea tipurilor abstracte de date prin tipuri reale de date. Un procedeu corect cere şi existenţa unui prototip care descrie funcţia reală necesară. Dacă funcţia-şablon este descrisă mai sus de locul utilizării, atunci prototipul funcţiei reale poate să lipsească. Iată forme de utilizare a funcţiei-şablon din exemplul anterior:. . .

60

Page 61: POO - Curs Doc

void main(){

int el_max(int * , int);float el_max(float *, int);int ti[4]={-240,4,330,7};float tf[3]={1.2, -3.47, 27.5};double td[4]={0,1.12,2.45,3.27};cout << el_max(ti, 4)<< endl;cout << el_max(tf, 3)<< endl;cout << el_max(td, 4)<< endl;

}

Nu totdeauna algoritmul descris de funcţia-şablon este acel care poate fi aplicat absolut pentru toate tipurile reale de date utilizate în program. Unele date de anumite tipuri pot necesita algoritmi diferiţi. În asemenea situaţii, pe lângă funcţii-şablon sunt admise şi funcţii obişnuite, care au acelaşi nume cu funcţiile-şablon şi corespund algoritmilor diferiţi. Dacă există şi funcţii-şablon şi funcţii obişnuite cu acelaşi nume, prioritare sunt funcţiile obişnuite elaborate pentru unele combinaţii de date de anumite tipuri concrete. De exemplu, suma a două variabile de tip char ar putea însemna un şir de caractere constând din valorile acestor variabile, ceea ce se deosebeşte desigur de suma a două variabile numerice. Pentru variabile numerice de tip int, short, long, float etc., este realizată o funcţie-şablon, iar pentru variabile de tip char este propusă o funcţie obişnuită, care realizează algoritmul necesar:

- variabile numerice:template<class Ta>T suma (Ta x, Ta y){

return (x+y);}

- variabile de tip char:char* suma (char x, char y){

char *sir;sir = new char[3];sir[0] = x;sir[1] = y;sir[2] = ’\0’;return sir;

}

Trebuie de subliniat faptul că funcţiile-şablon pot fi supraîncărcate tot aşa ca şi funcţiile obişnuite.Dacă până acum au fost propuse exemple de funcţii-şablon care conţineau doar un singur tip

abstract, în continuare este prezentat un exemplu conţinând mai multe tipuri abstracte.

Exemplu. De alcătuit o funcţie-şablon care permite compararea unei valori cu suma elementelor unui tablou. template<class Ta1, class Ta2>int compar_suma (Ta1 *tablou, Ta2 val, int numEl){

int i;Ta1 suma=0;for(i=0; i<numEl; i++) suma+=tablou[i];return (val-suma);

}

Un alt mecanism ce uşurează mult procesul de proiectare şi implementare a claselor este mecanismul claselor-şablon. Clasele-şablon permit crearea unei mulţimi de clase asemănătoare după

61

Page 62: POO - Curs Doc

funcţionalitate care se deosebesc doar după tipul datelor prelucrate. Toate funcţiile membre ale unei clase-şablon sunt funcţii-şablon. Pentru a descrie o clasă-şablon, este utilizată următoarea schemă generală:template<class Ta1, class Ta2, …, class Tak>class nume_cl_sb{

. . .// membri ai clasei. . .

};

unde Ta1, Ta2, …., Tak sunt tipuri abstracte de date, care pot servi la descrierea tipurilor date-membre, a tipurilor valorilor returnate de funcţiile-membre, a tipurilor parametrilor fictivi, a tipurilor variabilelor locale ale funcţiilor membre. Pentru descrierea funcţiilor membre ale unei clase-şablon este utilizat modelul ce urmează:

template<class Ta1, class Ta2, …, class Tak>tip_r nume_cl_sb<Ta1, Ta2, …, Tak>::nume_fn(lista_pf){

. . .//instructiuni. . .

}

unde nume_fn este o oarecare funcţie-membră, iar lista_pf reprezintă lista parametrilor fictivi. Pentru a crea obiecte în baza unei clase-şablon, mai întâi tipurile abstracte sunt înlocuite cu tipuri concrete de date, obţinând o clasă care prelucrează date de tipuri concrete, iar apoi este aplicat în mod obişnuit constructorul clasei pentru a finaliza acţiunea:nume_cl_sb<Tc1, Tc2, …, Tck> nume_ob(lista_pr);

unde Tc1, Tc2, …, Tck sunt tipuri concrete de date, nume_ob reprezintă numele obiectului creat, iar lista_pr este lista parametrilor reali.

Exemplu. De alcătuit un program în care este realizată clasa-şablon stiva, creând mai apoi stive concrete de diferite tipuri. template<class T>class stiva{

T *stv ; // stivaint dim; // dimensiunea stiveiint vst; // varful stivei

public:stiva(int d);~stiva();T push (T el);T pop ( );friend ostream & operator << (ostream & fl, stiva st);

};//------------template <class T>stiva <T> :: stiva (int d){

stv=new T[d];if(stv==NULL) dim=0;else dim=d;vst=0;

}//------------template

62

Page 63: POO - Curs Doc

<class T>stiva <T> :: ~stiva (){

if(stv !=NULL) delete [] stv;

}//------------template<class T>T stiva <T> :: push (T el){

if (vst<dim){

stv[vst]=el;vst ++;return el;

}else

return –1;}//------------template <class T>T stiva <T> :: pop ( ){

if (vst>0){

vst -- ;return stv[vst];

}else

return –1;}//------------template<class T>ostream & operator <<(ostream &fl, stiva <T> st){

for (int i=0; i<st.vst; i++)fl << st.stv[i] << ’ ’ ;

return fl;}

Memorând implementarea clasei în fişierul stiva_p.hpp, includem acest fişier în programul principal, efectuând o serie de operaţii cu diferite tipuri de stive:#include <iostream.h>#include "stiva_p.hpp"

void main (){

stiva<int> si(10);stiva<long> sl(15);stiva<float> sf(20) ;for (int i=0; i<10; i++){ si.push(i);

sl.push(i+100000); sf.push(float i/10.0);}cout << si << endl;cout << sl << endl;cout << sf << endl;si.pop() ;cout << si << endl ;

}

63

Page 64: POO - Curs Doc

Remarcă: Stiva generică realizată este orientată la descrierea stivelor pentru tipuri numerice de date.

§18. Realizarea conceptului de polimorfism

Un concept important în programarea orientată pe obiecte este noţiunea de polimorfism. Cuvântul polimorfism provine de la îmbinarea a două rădăcini polys ce înseamnă „numeros” şi morphe, ce înseamnă „formă”. În contextul programării polimorfism înseamnă o „singură interfaţă – mai multe forme de realizare”. O formă de polimorfism ar fi supraîncărcarea funcţiilor şi a operatorilor. În acest caz, interfaţa este numele funcţiei sau al operatorului, iar formele realizate sunt algoritmii diferiţi descrişi de fiecare funcţie în parte.

O formă mai complexă de polimorfism este obţinută utilizând conceptul de moştenire şi funcţii virtuale. În acest mecanism, sunt implicate cel puţin două tipuri de date:

- tipul ce corespunde clasei de bază;- tipurile ce corespund claselor derivate.

În tipul de bază sunt definite o serie de funcţii membre virtuale. O funcţie virtuală are proprietatea de a putea fi schimbată în cadrul tipurilor derivate din clasa de bază, schimbându-i sensul după necesitate în clasa derivată. O funcţie virtuală este introdusă prin intermediul cuvântului-cheie virtual:

virtual tipr nume_fv(lista_pf);

Pentru ca să fie realizat conceptul de polimorfism, în clasa derivată prototipul funcţiei trebuie să fie identic cu prototipul descris în clasa de bază. La cea mai mică deosebire dintre prototipuri polimorfismul nu va fi realizat. Va fi descrisă schema ce generează polimorfism, pornind de la următoarea schemă de moştenire:

Clasa de bază conţine o serie de funcţii virtuale:class cl_baza{. . . virtual tipr1 nume_fv1(lista_pf1);. . . virtual tiprn nume_fvn(lista_pfn);. . . };

class cl_der1 : public cl_baza{. . . [virtual tipr1 nume_fv1(lista_pf1);]. . . [virtual tiprn nume_fvn(lista_pfn);]. . . };. . . class cl_derk : public cl_baza{. . . [virtual tipr1 nume_fv1(lista_pf1);]. . . [virtual tiprn nume_fvn(lista_pfn);]. . . };

cl_der1 cl_der2 . . . cl_derk

cl_baza

64

Page 65: POO - Curs Doc

Funcţiile virtuale din clasa de bază cl_baza pot fi implementate în clasele derivate, schimbându-le dacă este necesar sensul. Totodată, există posibilitatea de a păstra implementarea din clasa de bază şi în unele clase derivate. În acest caz, pur şi simplu nu sunt implementate funcţiile virtuale în clasele date.tipr1 cl_baza :: nume_fv1 (lista_pf1){ . . //realizare01 .}. . .tiprn cl_baza :: nume_fvn (lista_pfn){ . . //realizare0n .}//--------------tipr1 cl_der1 :: nume_fv1 (lista_pf1){ . . //realizare11 .}. . .tiprn cl_der1 :: nume_fvn (lista_pfn){ . . //realizare1n .}//--------------. . .//--------------tipr1 cl_derk :: nume_fv1 (lista_pf1){ . . //realizarek1 .}. . .tiprn cl_derk :: nume_fvn (lista_pfn){ . . //realizarekn .}//--------------

După implementarea funcţiilor-membre pot fi generate obiectele necesare. Pentru a beneficia de polimorfism, trebuie de creat un pointer de tip clasa de bază. Încărcând în acest pointer adresa unui obiect, va fi posibil ca prin el să apelăm funcţii membre. În acest pointer pot fi încărcate adrese de obiecte de tip clasă de bază sau obiecte de tipul uneia dintre clasele derivate. Apelând prin intermediul acestui pointer funcţiile virtuale, va fi obţinut efectul de polimorfism. Interfaţa în cazul acesta va fi numele pointerului în combinaţie cu numele funcţiei virtuale, iar formele vor fi realizările funcţiei virtuale, fie realizarea din clasa de bază, fie realizările din clasele derivate. Dacă pointerul conţine adresa unui obiect de tipul clasei de bază, atunci la apelarea unei funcţii virtuale va fi apelată anume funcţia realizată în clasa de bază. Dacă însă pointerul conţine adresa unui obiect de tipul unei clase derivate, atunci la apelarea unei funcţii virtuale va fi apelată funcţia realizată anume în clasa derivată. Deci pointerul este sensibil la tipul obiectului a cărui adresă o conţine, determinând şi apelând funcţia care corespunde obiectului dat. În continuare, sunt reprezentate schematic ideile expuse mai sus:cl_baza *p_baza, ob_baza(. . .);cl_der1 ob_der1(. . .);

65

Page 66: POO - Curs Doc

cl_der2 ob_der2(. . .);. . .cl_derk ob_derk(. . .);p_baza=&ob_baza;p_baza->nume_fv1(lista_pr); //va fi apelata realizare01p_baza=&ob_der1;p_baza->nume_fv1(lista_pr); //va fi apelata realizare11p_baza=&ob_der2;p_baza->nume_fv1(lista_pr); //va fi apelata realizare21. . .p_baza=&ob_derk;p_baza->nume_fv1(lista_pr); //va fi apelata realizarek1

Fixând funcţiile virtuale ale unei clase, trebuie să ţinem cont de următoarele reguli:- constructorii unei clase nu pot fi nicidecum funcţii virtuale; - destructorul unei clase poate fi virtual.

Este cunoscut faptul că funcţiile-membri ale unei clase sau, altfel spus, metodele clasei sunt apelate prin intermediul unui obiect de tipul clasei date sau prin intermediul unui pointer. Desigur că pentru apelare este necesară o coordonare între adresele obiectului sau a pointerului şi a metodei, adică crearea unei legături corecte. Legarea unei metode de un obiect de anumit tip se face în procesul compilării şi o astfel de legătură se numeşte legătură statică (static binding), fiindcă este cunoscut şi obiectul, şi metoda apelată. Legarea metodei de pointer în lipsa polimorfismului este efectuată tot în timpul compilării după tipul pointerului şi de aceea este, de asemenea, o legătură statică. Legarea metodei de pointer în procesul polimorfismului poate fi efectuată doar în timpul rulării programului, fiindcă informaţia despre tipul pointerului este insuficientă, iar până la rulare nu este cunoscut obiectul a cărui adresă va fi încărcată în pointer şi deci nu poate fi determinată corect nici metoda apelată. De aceea o astfel de legătură se numeşte legătură dinamică (dynamic binding).

Pentru a putea determina corect metoda polimorfică apelată, clasele care conţin metode virtuale au un element suplimentar asociat cu ele, şi anume un tabel al funcţiilor virtuale. Cu ajutorul acestui tabel este calculată corect adresa metodei apelate. Ca rezultat, obiectele create în baza unei clase fără funcţii virtuale ar fi mai mici decât obiectele create în baza unei clase similare, dar care conţine funcţii virtuale. Totodată, apelarea metodelor nevirtuale este mai rapidă decât apelarea metodelor virtuale.

§19. Clase abstracte

Un tip de funcţii virtuale speciale sunt funcţiile virtuale abstracte. Prototipurile acestor funcţii se deosebesc de prototipurile funcţiilor ne-abstracte prin faptul că se termină cu ”=0”:virtual tipr nume_fv(lista_pf)=0; // functie abstracta

Funcţiile abstracte pot fi doar parte componentă a unor clase. Clasele care conţin măcar o funcţie abstractă se numesc clase abstracte. Pot exista clase abstracte care conţin doar funcţii abstracte. Caracteristica principală a funcţiilor abstracte constă în faptul că ele nu sunt realizate în clasa lor de declarare. Deci, clasa conţine doar descrierea prototipului funcţiei, fără a fi definit şi corpul funcţiei. Astfel de funcţii se utilizează atunci, când clasă este una foarte abstractă şi la nivelul dat de abstractizare este imposibil de a defini algoritmic funcţia. Ca rezultat al existenţei în clasa abstractă a funcţiilor nedefinite este interzis de a crea obiecte în baza unei clase abstracte. O clasă abstractă poate fi folosită doar ca clasă de bază în procesul de moştenire. Deci în procesul de generare a unei clase derivate una sau mai multe clase de bază pot fi şi clase abstracte. Dacă în clasa derivată obţinută sunt definite toate funcţiile abstracte moştenite din clasele de bază, atunci obţinem o clasă obişnuită în baza căreia putem crea obiecte. Dacă însă în clasa derivată obţinută rămâne nedefinită măcar o funcţie abstractă, atunci clasă derivată va fi în continuare una abstractă. Pot exista situaţii când în procesul de moştenire sunt implicate doar clase abstracte. Deci, clasele abstracte sunt utilizate ca o interfaţă comună pentru derivarea diferitelor clase, care au o legătură cu ea.

66

Page 67: POO - Curs Doc

Exemplu. De alcătuit un program în care este realizată clasa abstractă figura din care sunt derivate clasele cerc şi patrat. Unele operaţii cu obiectele de tip cerc şi patrat sunt efectuate în bază de polimorfism. La descrierea claselor cerc şi patrat este utilizată clasa ajutătoare punct.

class punct {

int x;int y;

public:punct(int x1=0, int y1=0){ x=x1; y=y1; }

punct(punct & p){ x=p.x; y=p.y; }

int getx() {return x;} int gety() {return y;}

};//----------class figura{public :

virtual void afisare()=0;virtual void miscare(punct p)=0;

};//----------class cerc: public figura{

punct centru;int raza;

public:cerc(punct c, int r) : centru(c){ raza=r; }void afisare(); void miscare(punct p);

};//----------class patrat : public figura{

punct virf ;int lat;

public:patrat(punct v, int l) : virf(v){ lat=l; }void afisare(); void miscare(punct p);

};//----------void cerc :: afisare(){

circle(centru.getx(), centru.gety(), raza);

cerc pătrat

figură

67

Page 68: POO - Curs Doc

}//----------void cerc :: miscare(punct p){

int c=getcolor() ;setcolor(getbkcolor());afisare();centru=p;setcolor(c);afisare();

}//----------void patrat :: afisare(){

int x=virf.getx();int y=virf.gety();line(x, y, x+lat, y);line(x, y, x, y+lat);line(x+lat, y, x+lat, y+lat);line(x, y+lat, x+lat, y+lat);

}//----------

void patrat :: miscare(punct p){

int c=getcolor();setcolor(getbkcolor());afisare();virf=p;setcolor(c);afisare();

}

Implementarea claselor este memorată în fişierul cerc_pat.hpp, iar programul principal este memorat în fişierul cerc_pat.cpp, conţinutul căruia urmează.#include <iostream.h>#include <conio.h>#include <graphics.h>#include ”cerc_pat.hpp”

void main(){

int rg=DETECT, srg, er;figura *pf; initgraph(&rg, &srg, "");er=graphresult();if(er==grOk){ punct pc(100,100), pv(10,17);

cerc c1(pc, 20);patrat p(pv, 200);pf = &c1;pf->afisare();pf = &p;pf->afisare();pf->miscare(pc); getch();closegraph();

}}

§20. Membrii statici ai clasei

68

Page 69: POO - Curs Doc

Atunci când este necesară crearea membrilor comuni pentru toate obiectele unei clase se utilizează membri statici ai clasei.

O proprietate statică se obţine cu ajutorul cuvântului static plasat înaintea membrului dat:static tip nume_membru;

În interiorul clasei, proprietatea statică este doar declarată, iar pentru a o defini (adică a-i aloca memorie), ea trebuie să fie definită în exteriorul clasei, scriind:tip nume_clasa::nume_membru; //fără iniţializare

caz în care membrul dat va fi iniţializat cu valoarea zero, sau:tip nume_clasa::nume_membru=val; //cu iniţializare

caz în care membrul dat va fi iniţializat cu valoarea propusă val. Chiar dacă membrul static este de tip privat, definirea lui şi iniţializarea cu prima valoare se va face în exteriorul clasei.

În afară de proprietăţi statice, mai există şi funcţii membre statice. Ele se declară în interiorul clasei tot cu ajutorul cuvântului static:static tip nume_functie(lista_param);

În exteriorul clasei, ea este definită asemănător cu o funcţie ne-statică:tip nume_cl::nume_functie(lista_param){. . .}

Funcţiile statice au o serie de restricţii în utilizare: Nu au acces la membrii ne-statici ai clasei, de aceea foarte des funcţiile statice sunt create pentru a

iniţializa membrii statici ai clasei. În cadrul funcţiilor de tip static, nu există acces la pointerul this.

Modul de apelare a funcţiilor statice se deosebeşte puţin de apelarea funcţiilor ne-statice. Ele pot fi apelate prin metoda obişnuită, adică prin intermediul unui obiect:nume_obiect.nume_functie(lista_param)

sau poate fi apelată direct la nivel de clasă astfel:nume_clasa::nume_functie(lista_param)

Funcţiile statice sunt deseori aplicate pentru a prelucra membrii statici ai clasei. Un exemplu de utilizare a membrilor statici poate fi realizarea unui contor al obiectelor create în baza unei clase. În continuare, este prezentat un exemplu de acest tip.//student.hppclass student{ unsigned long id; static unsigned contor;public: student(unsigned long i) { id=i; contor++; } student(student &s) { id=s.id; contor++; } ~student() { contor--; } static unsigned GetContor ();};unsigned student::GetContor(){

return contor;}unsigned student::contor=0;//definirea membrului static //în exteriorul clasei

Realizarea clasei este înscrisă în fişierul student.hpp şi va fi utilizată în programul student.cpp care urmează.//student.cpp#include <iostream.h>

69

Page 70: POO - Curs Doc

#include ”student.cpp”void creare_st(){ student s(1759); cout << s.GetContor() << endl;}void main(){ student A(529478); cout << A.GetContor() << endl; //afişează 1 student B(A); cout << student::GetContor()<<endl; //afişează 2 creare_st(); //afişează 3 şi deoarece la ieşire

//din funcţie obiectul creat se //distruge la ultima apelare a //funcţiei GetContor() se va afişa 2 cout << student::GetContor()<<endl; //afişează 2}

O altă aplicare a funcţiilor statice poate fi realizarea de interfeţe, atunci când diferite platforme au diferite realizări funcţionale de interacţiune cu anumite resurse tehnice sau de program şi este necesară crearea de versiuni ale aplicaţiei pentru aceste platforme. De aceea se proiectează şi se realizează o interfaţă, adică nişte funcţii cu ajutorul cărora va fi programată aplicaţia. Pentru a realiza diferite versiuni pentru diferite platforme, va fi schimbată doar realizarea funcţiilor din interfaţă, realizarea aplicaţiei rămânând neschimbată.

Ca exemplu ar putea fi luat lucrul cu regimul grafic. Pentru diferite sisteme de operare, există diferite funcţii ce realizează sarcinile de desenare a diferitelor elemente geometrice, de iniţializare şi de închidere a regimului grafic.

În continuare este propusă o clasă-interfaţă EcranGr, în care sunt realizate doar câteva funcţii statice init(), linie(), cerc(), punct(), inchidere() pentru lucru în regim grafic. Pentru a nu putea crea obiecte în baza clasei EcranGr, constructorul clasei este unul privat. În continuare este prezentată o versiune a acestei clase.

class EcranGr{public: static void init (); static void linie(int x1, int y1, int x2, int y2); static void cerc(int x, int y, int r); static void punct(int x, int y); static void inchidere();private: EcranGr() { }; unsigned static culoare;};

unsigned EcranGr::culoare=15;

void EcranGr::init(){ int dr=DETECT, rg; initgraph (&dr, &rg, ” ”);}

void EcranGr::linie(int x1, int y1, int x2, int y2){ line(x1, y1, x2, y2);}

void EcranGr::cerc(int x, int y, int r){ circle(x, y, r);

70

Page 71: POO - Curs Doc

}

void EcranGr::punct (int x, int y){ putpixel (x, y, culoare);}

void EcranGr::inchidere(){ closegraph();}

Funcţiile date pot fi aplicate la realizarea algoritmului necesar de desenare.

§21. Enumerări, uniuni, structuri de date în C++

Toate aceste noţiuni au semnificaţii similare cu cele din limbajul C, dar mai au şi unele trăsături caracteristice doar limbajului C++.

Din cum se ştie, o enumerare creează nişte etichete în spatele cărora sunt, de fapt, nişte constante întregi. Descrierea generală a unei enumerări este exprimată astfel:enum [nume_enum] {nume_et1[=val1], nume_et2[=val2], …, nume_etk[=valk]} [lista_var];

În C++ enumerările se utilizează deseori pentru definirea unor constante utilizate în interiorul unei clase, de aceea astfel de enumerări sunt descrise în interiorul clasei respective. Dacă enumerarea se utilizează doar în interiorul clasei, atunci ea poate fi definită în zona privată a clasei. De exemplu:class student {

enum {obiecte=7, semestre=10};int note[semestre][obiecte];. . .

};

Dacă unele etichete vor fi utilizate şi în exteriorul clasei, atunci enumerarea este descrisă în zona publică a clasei. Pentru a utiliza una dintre etichetele enumerării în exteriorul clasei, se foloseşte următoarea sintaxă:nume_cl::nume_et

Dacă se doreşte a defini variabile de tip enumerare în baza unei enumerări declarate în interiorul unei clase, se procedează astfel:nume_cl::nume_enumerare var1, var2, ..., vark;

Exemplificarea se va face în baza clasei ios, în zona publică a căreia este declarată următoarea enumerare:enum { skipws = 0x0001, left = 0x0002, right = 0x0004, internal = 0x0008, dec = 0x0010, oct = 0x0020, hex = 0x0040, showbase = 0x0080, showpoint = 0x0100, uppercase = 0x0200, showpos = 0x0400, scientific= 0x0800, fixed = 0x1000, unitbuf = 0x2000,

71

Page 72: POO - Curs Doc

stdio = 0x4000};

În baza acestei enumerări se fixează o serie de fanioane utilizate la afişarea informaţiei pe ecran, de exemplu:cout << setiosflags(ios::hex) << 100;

Forma generală de descriere a unei structuri:struct [nume_struct]{

tip1 cimp1;. . .tipN cimpN;

} [lista_var];

În C++ o structură mai poate avea şi funcţii încorporate, astfel ea devenind foarte asemănătoare cu o clasă. Deosebirile dintre structuri şi clase:

a) Dacă în cadrul unei clase nu este folosit modificatorul de acces, se subînţelege privat, pe când în cadrul unei structuri se subînţelege public.

b) Dacă în procesul moştenirii modificatorul de moştenire nu este indicat şi entitatea derivată este o structură, atunci se subînţelege modificatorul de moştenire public, iar în cazul unei clase derivate se subînţelege modificatorul de moştenire private.

O uniune este descrisă într-un mod similar cu o structură de date:union [nume_uniune]{

tip1 cimp1;. . .tipN cimpN;

} [lista_var];

În C++ o uniune poate avea şi funcţii încorporate, dispunând astfel şi de funcţionalitate. Cu referinţă la modelul obiectual, uniunile au multe restricţii şi deosebiri în comparaţie cu clasele:

a) Dacă în cadrul unei uniuni nu este folosit modificatorul de acces, se subînţelege public.b) Uniunile nu pot fi implicate în procesul de moştenire nici în rol de entitate de bază, nici în rol de

entitate derivată.c) O uniune nu poate avea în calitate de membru un obiect al cărui clasă are operatorul =

supraîncărcat.Pot fi declarate uniuni globale statice anonime, ele având rolul unor zone de memorie comună

pentru toate câmpurile care intră în componenţa fiecărei uniuni. Pentru a accesa câmpurile din componenţa unor astfel de uniuni, operatorul rezoluţiei :: este plasat în faţa denumirii câmpului necesar. Iată un exemplu care ilustrează cele spuse:#include<iostream.h>static union{

int i;long l;

};void main(){ ::l=0; ::i=90; cout<< ::l << endl;}

Întrucât câmpurile i şi l ocupă aceeaşi zonă de memorie, rezultatul afişării va fi 90.

72

Page 73: POO - Curs Doc

§22. Prelucrarea fişierelor în C++

Fluxurile de intrare/ieşire în C++ se tratează în baza unor clase şi a unor obiecte special create pentru asemenea acţiuni. Clasele date formează o ierarhie de moştenire, îmbogăţindu-se de la nivel la nivel cu noi elemente necesare în procesul de prelucrare a fluxurilor de date. În continuare, este prezentată o ierarhie a celor mai importante clase.

O componentă a interacţiunii cu fluxurile de intrare/ieşire o reprezintă interacţiunea cu fişierele. Lucrul cu fişierele este organizat în baza claselor ifstream, ofstream şi fstream. Clasa ifstream este destinată operaţiei de citire din fişiere. Clasa ofstream este menită pentru operaţia de scriere în fişiere, clasa fstream este destinată atât operaţiei de citire din fişiere, cât şi operaţiei de scriere în fişiere. Pentru a putea utiliza clasele date, este necesar a include fişierul antet fstream.h care conţine descrierile necesare referitoare la clasele date.

Pentru a accesa un fişier de citire sau scriere, mai întâi se creează obiecte fie în baza clasei ifstream, fie a clasei ofstream, fie a clasei fstream:ifstream nume_ob;ofstream nume_ob;fstream nume_ob;

unde nume_ob reprezintă denumirea obiectului definit fie pentru citire, fie pentru scriere, fie pentru citire şi scriere. Obiectul definit nu este legat de oarecare fişier, de aceea, pentru crearea legăturii cu fişierul necesar, este apelată funcţia-membru open(), având următorul prototip:void open(const char *nume_f, int mod_des);

unde parametrul nume_f reprezintă adresa şirului conţinând numele fişierului deschis, iar mod_des descrie modul de deschidere a fişierului. Modul de deschidere este descris printr-o serie de constante întregi, exprimate prin intermediul unei enumerări, care face parte din clasa ios. De exemplu, în fragmentul ce urmeazăifstream cit;cit.open("L.cpp", ios::in);

este efectuată deschiderea fişierului L.cpp cu modul de deschidere ios::in, adică pentru citire. În continuare sunt prezentate toate valorile posibile care pot descrie modul de deschidere:

fstream

ofstreamifstream

streamable

ostreamistream

ios

73

Page 74: POO - Curs Doc

Valori posibile ale modului de deschidere

Semantica impusă de valoare

ios :: in

deschiderea fişierului pentru citire

ios :: out deschiderea fişierului pentru scriere

ios :: trunc deschiderea fişierului cu trunchierea informaţiei

ios :: ate deschiderea fişierului cu plasarea cursorului la sfârşitul fişierului

ios :: app deschiderea fişierului cu impunerea scrierii doar la sfârşitul fişierului

ios :: nocreate deschiderea unui fişier existent, interzicându-se crearea lui, dacă fişierul nu există

ios :: noreplace deschiderea fişierului, interzicându-se înlocuirea unui fişier existent

ios :: binary deschiderea fişierelor în regim binar

Valorile descrise în tabel pot fi combinate, obţinând efectul reunirii semanticilor a două sau a mai multe valori. Combinarea fanioanelor se face cu ajutorul operaţiei de acţiune pe binari | (sau).

În continuare sunt prezentate o serie de exemple care demonstrează unele dintre proprietăţile descrise mai sus:

- deschiderea unui fişier pentru scriereofstream sc;sc.open("t.txt", ios::out);

- deschiderea unui fişier pentru citire şi scriere în regim binarfstream cit_sc;cit_sc.open("a.txt", ios::in | ios::out | ios::binary);

Trebuie de subliniat faptul că clasele ifstream, ofstream, fstream conţin mai mulţi constructori. O variantă importantă de constructor cu parametri efectuează crearea obiectului realizând şi deschiderea fişierului asociat cu obiectul dat.ifstream n_ob(const char *nume_f, int mod_des=ios::in);ofstream n_ob(const char *nume_f, int mod_des=ios::out);fstream n_ob(const char *nume_f, int mod_des);

În cele ce urmează, este re-scris exemplul anterior creând un obiect asociat cu fişierul ”a.txt” şi fiind destinat citirii şi scrierii în regim binar: fstream cit_sc("a.txt", ios::in | ios::out | ios::binary);

După ce fişierul este asociat cu obiectul necesar, pot fi efectuate o serie de operaţii. Sunt, desigur, foarte importante operaţiile de citire şi operaţiile de scriere. Operaţiile date pot fi efectuate atât cu ajutorul unor operatori, cât şi cu ajutorul unor funcţii. Sunt aplicaţi operatorii de inserţie << şi extragere >> utilizaţi şi cu obiectele cout şi cin, caracterizaţi în unul din paragrafele anterioare. Deci, pentru a face scrierea într-un fişier, obiectul asociat cu fişierul acţionează operatorul de inserţie:nume_ob << date;

Pentru a face citirea dintr-un fişier, obiectul asociat cu fişierul acţionează operatorul de extragere:nume_ob >> nume_var;

De exemplu, în cele ce urmează vor fi scrise într-un fişier câteva valori numerice:ofstream scriu("val.dat", ios::out);float f=-45.23;

74

Page 75: POO - Curs Doc

. . .scriu << f << ’ ’ << 24.3;. . .

La necesitate, într-un mod asemănător, se poate citi din fişier:ifstream citire("val.dat", ios::in);float f;. . .citire >> f;cout << f << endl;. . .

Una dintre funcţiile referitoare la operaţiile de citire este funcţia getline(), caracterizată în unul dintre primele paragrafe. Ea are următorul prototip:istream & getline(char *sir, int max, char delim=’\n’);

Mai există o clasă de funcţii get(), a căror prototipuri sunt prezentate mai jos:int get();istream & get(char & car);istream & get(char *sir, int max, char delim=’\n’);

Varianta a treia este aproape identică cu funcţia getline(), atât doar că la citire ea nu scoate delimitatorul din flux. Primele două funcţii permit citirea unui caracter din flux.

Iată câteva exemple, utilizând funcţiile prezentate mai sus:- citirea unui sir de caractere cu funcţia getline() (delimitator este simbolul ’_’)

ifstream cit("date.dat", ios::in);. . .char sir[100];cit.getline(sir, sizeof(sir), '_');

- citirea unui caracter cu funcţia get()char c;c=cit.get( );

- citirea unui caracter cu o altă variantă a funcţiei get()char c;cit.get(c);

O altă categorie de funcţii este reprezentată de funcţiile pentru citire/scriere neformatată. Ele permit citirea sau scrierea unui fragment de date fără oarecare transformare a lor. Prototipurile acestor funcţii sunt asemănătoare, fiind prezentate în cele ce urmează:

- prototipul funcţiei de citireint read(char *dom, int num_car);

- prototipul funcţiei de scriereint write(char *dom, int num_car);

Parametrul dom reprezintă adresa domeniului unde este citită sau de unde este scrisă informaţia, iar parametrul num_car reprezintă numărul de caractere respectiv citite sau scrise. Funcţiile returnează numărul de caractere de facto citite sau scrise. Dacă valoarea returnată este mai mică decât valoarea parametrului num_car, atunci fie s-a produs o eroare, fie s-a ajuns la sfârşitul fişierului în procesul operaţiilor de citire. În exemplul ce urmează, datele citite de la tastatură sunt scrise în fişierul Doc.txt, efectuându-se şi controlul unei scrieri corecte în fişier:char sir[100];int num;ofstream sc("Doc.txt", ios :: out);. . .cin.getline(sir, sizeof(sir));num=sc.write(sir, strlen(sir));if(num < strlen(sir)) cout << ”Eroare de scriere” << endl;

O altă categorie importantă o formează funcţiile de control al poziţiei curente. Poziţia curentă este locul unde va fi scrisă sau de unde se va citi informaţia din fişier. Determinarea poziţiei curente poate fi

75

Page 76: POO - Curs Doc

efectuată cu ajutorul funcţiei tellg() pentru fluxurile de citire şi tellp(), pentru fluxurile de scriere. Iată prototipurile acestor funcţii:long tellg();long tellp();

Ambele funcţii returnează o valoare numerică cuprinsă între 0 şi lungumea fişierului minus unu. Pentru schimbarea poziţiei curente, în cadrul fluxului este utilizată funcţia seekg() pentru

fluxurile de citire şi seekp() pentru fluxurile de scriere. Aceste funcţii sunt descrise de următoarele prototipuri:int seekg(int pozitie, int origine);int seekp(int pozitie, int origine);

unde parametrul pozitie determină valoarea poziţiei curente, iar parametrul origine determină în raport cu ce este fixată poziţia curentă şi poate lua trei valori. Sensul acestor valori este descris în următorul tabel:

Valori posibile ale parametrului origine

Semantica impusă de valoare

ios :: beg poziţia curentă este fixată în raport cu începutul fişierului

ios :: cur poziţia curentă este fixată în raport cu poziţia curentă

ios :: end poziţia curentă este fixată în raport cu sfârşitul fişierului

Pentru a determina starea fluxului de date, sunt utilizate o serie de funcţii care oferă unele informaţii necesare. Funcţia eof() permite determinarea faptului dacă s-a ajuns sau nu la sfârşitul fişierului. Funcţia fail() permite a determina dacă s-a produs sau nu o eroare nefatală. Funcţia bad() permite a determina dacă s-a produs sau nu o eroare fatală. Funcţia good() permite a determina dacă operaţia anterioară aplicată asupra fluxului s-a efectuat sau nu cu succes. Funcţiile respective au următoarele prototipuri:int eof();int fail();int bad();int good();

Funcţiile date returnează fie valoarea 0, fie valoarea 1, în dependenţă de evenimentul produs. Tabelul ce urmează descrie valorile returnate de funcţii.

Numele funcţiei

Valoarea returnată

Semantica valorii returnate

eof()

0 nu s-a ajuns la sfârşitul fişierului

1 s-a ajuns la sfârşitul fişierului

fail() 0 nu s-a produs o eroare nefatală

1 s-a produs o eroare nefatală

bad() 0 nu s-a produs o eroare fatală

1 s-a produs o eroare fatală

good()

0 s-a produs o eroare

1 operaţie efectuată cu succes

Urmează un exemplu în care sunt utilizate unele dintre ideile descrise anterior.

76

Page 77: POO - Curs Doc

Exemplu. De alcătuit un program în care sunt realizate operaţiile de transcriere a informaţiei dintr-un fişier cu caractere majuscule şi cu caractere minuscule. Operaţiile sunt realizate prin două metode: una utilizează două fluxuri de date, unul de intrare şi altul de ieşire, iar alta utilizează doar un singur flux, dar de intrare/ieşire.#include<fstream.h>#include<stdlib.h>#include<ctype.h>#include<string.h>#include<conio.h>

void fisierMaj(char *numef){

int numCar;char linie[20];ifstream cit(numef);if(cit.fail()){ cerr << "Eroare deschidere."; exit(0);}ofstream scr(numef);if(scr.fail()){ cerr << "Eroare deschidere."; exit(0);}

for(;!cit.eof();){ cit.getline(linie,sizeof(linie)); numCar=cit.gcount(); for(int i=0; *(linie+i); *(linie+i++)=toupper(*(linie+i))); scr << linie; if(numCar!=strlen(linie)) scr << ’\n’;}scr.close();cit.close();

}//-----------------void fisierMin(char *numef){

long pozitie;int numCar;char linie[20];

fstream scr(numef,ios::out | ios::in);if(scr.fail()){ cerr << "Eroare deschidere."; exit(0);}

for(;!scr.eof();){ pozitie=scr.tellg(); scr.getline(linie,sizeof(linie)); numCar=scr.gcount(); for(int i=0; *(linie+i); *(linie+i++)=tolower(*(linie+i))); scr.seekg(pozitie, ios::beg); scr << linie; if(numCar != strlen(linie))

77

Page 78: POO - Curs Doc

scr << ’\n’;}scr.close();

}//-----------------void main(void){

char nume[]=”import.txt”;

fisierMin(nume);fisierMaj(nume);getch();

}

Pentru a realiza scopul propus, au fost alcătuite două funcţii fisierMin() şi fisierMaj() pentru a face transcrierea respectiv cu minuscule şi cu majuscule. Prima funcţie a folosit două fluxuri, pe când a doua funcţie a folosit doar un flux, dar a mai utilizat o variabilă în care se memora la citire poziţia curentă pentru a şti locul de scriere a informaţiei transformate. Prima funcţie nu a avut necesitate în o astfel de variabilă.

§23. Tratarea excepţiilor

Un program elaborat corect înseamnă nu doar prelucrări ale datelor corecte, ci şi prelucrarea datelor eronate. Scenariile descrise de programe trebuie să conţină fragmente care ar prelucra şi date generatoare de erori. Procesul de prelucrare a posibilelor erori este unul foarte anevoios: trebuie evidenţiate situaţiile generatoare de erori, iar apoi trebuie de prelucrat fiecare situaţie în parte. Situaţiile generatoare de erori sunt, de fapt nişte situaţii excepţionale şi de aceea prelucrarea erorilor poate fi numită şi tratare a excepţiilor.

Limbajul C++ are un mecanism propriu de tratare a excepţiilor. Acest mecanism se bazează pe conceptul de clasă. Deci o excepţie este descrisă de o clasă, având o serie de proprietăţi şi de operaţii caracteristice. Din punctul de vedere al programării, a genera o excepţie înseamnă a crea un obiect cu anumite proprietăţi şi operaţii concrete, care descrie un anumit tip de erori.

Pentru prelucrarea excepţiilor, există trei construcţii specializate: try{…}, throw şi catch(){…}. Fiecare dintre construcţiile date îşi are rolul său specific în evidenţierea şi prelucrarea excepţiilor. Blocurile de instrucţiuni suspectate de a putea genera erori sunt plasate între acoladele instrucţiunii try, adică formează corpul ei: try{ instr_1; instr_2; . . . instr_k;}

Dacă valorile datelor sunt în contradicţie cu modul lor de prelucrare, trebuie de generat o excepţie. Generarea unei excepţii este realizată cu ajutorul instrucţiunii throw. Instrucţiunea dată are ca parametru un obiect de tipul ce corespunde excepţiei. De aceea poate fi scristhrow obiectExceptie;

unde obiectExceptie este un obiect creat în baza clasei ce corespunde excepţiei. Deseori obiectul dat este creat chiar în cadrul instrucţiunii throw cum urmează în continuarethrow numeExceptie(lista_par);

unde numeExceptie este numele constructorului clasei care descrie excepţia, iar lista_par este lista parametrilor reali utilizaţi la generarea obiectului-excepţie. Deci numeExceptie este un tip de date, fie predefinit, fie definit. În cazul când este un tip predefinit, lista_par constă dintr-o singură valoare şi, de regulă, este omis din construcţie numele tipului predefinit, rămânând doar valoarea. De exemplu, poate fi scris

78

Page 79: POO - Curs Doc

throw int(3);

ceea ce este echivalent cuthrow 3;

Procesul de generare a excepţiei se numeşte lansarea excepţiei. Pentru a prelucra excepţiile lansate, după blocul try sunt plasate o serie de construcţii de tip

catch:catch(tip [nume_ob]){ instr_1; instr_2; . . . instr_k;}

unde tip este un tip de date predefinit sau definit de utilizator, care descrie tipul excepţiei, iar nume_ob este numele obiectului, care reprezintă excepţia şi care poate lipsi. Deci în acest caz toate excepţiile de acest tip vor fi tratate identic. Dacă se doreşte existenţa unei construcţii catch care ar prelucra orice tip de excepţii, în locul parametrului sunt plasate trei puncte (. . .):catch(. . .) set_instructiuni

În acelaşi loc al programului pot fi, de regulă, plasate mai multe construcţii catch, obţinând un pachet de astfel de construcţii. De aceea într-un astfel de pachet trebuie respectate o serie de reguli:

- există o singură excepţie care se referă la un tip de date anume;- nu are vreo importanţă ordinea de amplasare a excepţiilor care se referă la diferite tipuri de

date;- excepţia care se referă la toate tipurile de date, adică cea care conţine trei puncte

(. . .)este plasată ultima în secvenţa instrucţiunilor catch. În continuare, este prezentat un exemplu, excepţiile fiind de tipuri predefinite de date. Excepţiile

sunt lansate în funcţia f(). După blocul try sunt plasate trei blocuri catch(): unul pentru excepţii de tip int, altul pentru excepţii de tip double şi ultimul pentru prelucrarea excepţiilor de orice alt tip. #include<iostream.h>

void f(int i){

if(i<0) throw -1;if(i>=0 && i<100) throw 100.0;if(i>=1000) throw float(1000);cout<<i<<endl;

}//--------void main(){

int i;cin>>i;try{ f(i);}catch(int i){ cout<<i<<"-exceptie tip int\n";}catch(double){ cout<<"exceptie tip double\n";}catch(...)

79

Page 80: POO - Curs Doc

{ cout<<"exceptie generala\n";}

}

Fiindcă o excepţie este în caz general reprezentată printr-o clasă, urmează un exemplu de excepţie definită pentru a prelucra situaţia de inexistenţa unui fişier deschis pentru anumite operaţii. #include<fstream.h>#include<iostream.h>#include<conio.h>

class FisierInexistent{};

void main(){ char numePrenume[100]; try { ifstream cit("persoane.txt"); if(cit.fail()) { throw FisierInexistent(); } cit.getline(numePrenume,100); cout<<numePrenume<<endl; } catch(FisierInexistent e) { cout<<"Eroare de deschidere: fisier inexistent"; } getch();}

Dacă fişierul deschis nu există, va fi lansată excepţia FisierInexistent şi ca rezultat al prelucrării excepţiei date va fi afişat mesajul ”Eroare de deschidere: fisier inexistent”.

Clasa care reprezintă excepţia FisierInexistent este una foarte simplă, ne-conţinând nici un membru. Totuşi, ea poate fi proiectată, astfel încât să aibă o structură mai complexă. Poate conţine în calitate de informaţie numele fişierului, poate avea o serie de funcţii membru, de exemplu, constructori sau alte funcţii implicate în prelucrarea situaţiei excepţionale. Exemplul precedent va fi re-scris desfăşurând puţin clasa FisierInexistent. #include<fstream.h>#include<iostream.h>#include<conio.h>

class FisierInexistent{

char numeFisier[50];public:

FisierInexistent(char *nume){ strcpy(numeFisier, nume); }void MesajEroare(){ cout<<”Eroare: fisierul \””<<numeFisier <<”\” nu poate fi gasit!”<<endl; }};

void main(){

char numePrenume[100];char numef[]=”persoane.txt”;

try

80

Page 81: POO - Curs Doc

{ ifstream cit(nume); if(cit.fail()) { throw FisierInexistent(nume); } cit.getline(numePrenume,100); cout<<numePrenume<<endl;}catch(FisierInexistent e){ e.MesajEroare();}getch();

}

Din aceste exemple se văd doar unele caracteristici ale excepţiilor. Pentru a putea aprecia puterea excepţiilor, trebuie de ţinut cont că un algoritm serios poate consta dintr-o ierarhie de apeluri de funcţii. Eroarea poate surveni în orice punct al ierarhiei şi în acest caz este necesară o terminare corectă a proceselor lansate. O latură forte a mecanismului excepţiilor este anume terminarea corectă a proceselor lansate.

Este de menţionat faptul că construcţiile de tip try pot fi imbricate, adică incluse unele în altele. Cea mai simplă ierarhie de imbricare de acest tip ar consta din două niveluri şi ar putea avea următoarea reprezentare generalizată:try{ //sectorul A inceput . . . //sectorul A sfirsit try { //sectorul B inceput . . . //sectorul B sfirsit } //sectorul C inceput catch( ) { . . . } . . . //sectorul C sfirsit //sectorul D inceput . . . //sectorul D sfirsit}//sectorul E inceputcatch( ){ . . . }. . . //sectorul E sfirsit

O astfel de construcţie are mai multe scenarii de execuţie care depind de locul lansării excepţiei şi de tipul excepţiilor interceptate, care formează blocul de prelucrare intern şi blocul de prelucrare extern. Dacă excepţia este lansată în sectorul A sau D, atunci execuţia este transferată în sectorul E unde va fi selectată, dacă există, prelucrarea necesară după tipul ei. Dacă excepţia este lansată în sectorul B, atunci execuţia este transferată în sectorul C unde va fi selectată, dacă există, prelucrarea necesară după tipul ei, iar apoi se va trece la sectorul D. Dacă în sectorul C nu există blocul necesar de prelucrare, atunci se va trece la sectorul E, încercându-se găsirea blocului necesar de prelucrare aici. Este interesant faptul că o excepţie poate fi lansată şi în sectorul C în cadrul unuia dintre blocurile catch(). Poate fi lansată o excepţie nouă sau poate fi relansată excepţia veche. O relansare a excepţiei poate fi făcută pentru a schimba într-un

81

Page 82: POO - Curs Doc

fel scenariul de prelucrare a ei. La lansarea excepţiei în sectorul C execuţia va fi direcţionată în sectorul E.

82

Page 83: POO - Curs Doc

Bibliografie

1) G.Booch. Object-Oriented Design and Application. 1991.2) Kip R. Irvine. Object-Oriented Programming. - Prentice-Hall, 1997.3) Schildt Herbert. C++ Manual complet. – Bucureşti: Teora, 2001.4) Jamsa Kris. Succes cu C++. – Bucureşti: ALL EDUCATIONAL S.A.,1997.5) D.M.Popovici, I.M.Popovici, I.Tănase „C++.Tehnologia orientată pe obiect. Aplicaţii»,Teora 1996.6) Somnea D., Tutureanu D. „Iniţiere în C++. Programarea obiect orientată».Teora 1993.7) Negrescu Liviu. Limbajele C si C++ pentru începători. Limbajul C++ (vol. II). – Cluj-Napoca, 2000.8) Браен Страуструп. Язык програмирования С++, 3-е изд./Пер. с англ. – СПб., M., Невкий

Диалект – Издательство БИНОМ, 1999.9) В.В.Подбельский, С.С.Фомин. Програмирование на С++. M., Ф.С, 2000.

83