Notiuni de baza ale programarii orientate pe obiecte īn limbajul C++

1.Introducere

Exista limbaje concepute strict pe baza conceptelor programarii orientate pe obiecte (POO), de exemplu Simula sau Smalltalk. O alta abordare este de a adauga unor limbaje cu o popularitate bine stabilita, de exemplu Pascal si C, elementele tehnicii POO.

C++ combina avantajele oferite de limbajul C( eficienta, flexibilitate si popularitate) cu avantajele oferite de tehnica POO. Desi adopta principiile POO, C++ nu impune aplicarea lor stricta ( se pot scrie programe fara elemente POO). Conceptul fundamental īn C++ este clasa. Clasele sunt tipuri definite de utilizator, asigura īncapsularea datelor, garanteaza initializarea datelor, gestiunea memoriei controlata de utilizator, mecanisme pentru supraīncarcarea operatorilor.

C++ contine de asemenea īmbunatatiri ale limbajului C care nu sunt direct legate de POO, cum ar fi: tipul referinta, substitutia “in-line” a functiilor, nume de functii supraīncarcate, operatori pentru gestionarea memoriei libere.

2. Īmbunatatiri ale limbajului C introduse de C++

2.1.Transmiterea parametrilor

Īn C++ exista doua posibilitati de transmitere a parametrilor actuali catre o functie:
  1. mecanismul de transmitere prin valoare
  2. mecanismul de transmitere prin referinta

Primul este cunoscut, constituie modul standard de transmitere a parametrilor īn C.

Prin folosirea parametrilor formali referinta, se permite realizarea transferului prin referinta(transmiterea adresei) de o maniera similara celei din Pascal (parametrii VAR). Se elimina astfel necesitatea recurgerii la artificiul din C, adica utilizarea parametrilor formali pointeri, īn cazul īn care modificarile facute īn interiorul functiei asupra parametrilor trebuie sa ramāna si dupa revenirea din procedura.

Exemplu:

void schimba(int &a, int &b) {
int  aux=a; a=b; b=aux;
  }
Transferul prin referinta este util si atunci cānd parametrul are dimensiune mare (struct, class) si crearea īn stiva a unei copii a valorii lui reduce viteza de executie si īncarca stiva.

2.2.Parametrii cu valori implicite

C++ ofera posibilitatea declararii functiilor cu valori implicite ale parametrilor. La apelarea unei astfel de functii se poate omite specificarea parametrilor efectivi pentru acei parametri formali care au declarate valori implicite si se transfera automat valorile respective. Se pot specifica mai multe argumente cu valori implicite pentru o functie. Este obligatoriu ca numai ultimele argumente sa aiba astfel de valori si nu este permisa alternarea argumentelor cu si fara valori implicite.

Exemplu:

void fct(int, int=10);
void fct(int p1, int p2){
	tipareste(p1); tipareste(p2);
}

void main(void)  {
fct(1,2);
fct(3);
}
Rezultatul rularii acestui program va fi tiparirea numerelor:
1, 2, 3,10.

2.3.Functii inline

O tehnica utila pentru a mari performanta programelor ce folosesc multe functii de foarte mici dimensiuni, pentru care regia de apel este semnificativa īn raport cu timpul de executie al functiei propriu-zise, este folosirea functiilor inline. Functiile declarate ca inline vor fi expandate in-line la compilare, compilatorul generānd codul corespunzator functiei īn pozitia apelului, īn loc de a genera secventa de apel. Pentru a indica faptul ca o functie este de acest tip, se precede declaratia sau definitia ei cu cuvāntul cheie inline.

2.4.Supradefinirea (supraīncarcarea) functiilor

Supradefinirea reprezinta posibilitatea de a atribui unui simbol mai multe semnificatii, care pot fi deduse īn functie de context. Limbajul C++ permite supraīncarcarea (overloading) functiilor, adica existenta mai multor functii cu acelasi nume. Cānd compilatorul īntālneste un apel la o functie supraāncarcata, va determina care functie va fi apelata prin examinarea numarului si tipului argumentelor. Nu se verifica si tipul valorii returnate, deci doua functii supraīncarcate nu pot diferi doar prin valoarea returnata.

De exemplu:

void fct(int a) {
   cout << "functia 1" << a;
}

void fct(char *a) {
   cout << "functia 2" << a;
}

In particular, majoritatea operatorilor C++ pot fi priviti ca nume de functii si deci pot fi supraīncarcati. Antetul unei asemenea functii contine cuvāntul cheie operator īn fata simbolului operatorului.

2.5. Alocarea dinamica a memoriei folosind operatorii new si delete

Operatorii new si delete sunt similari functiilor din grupul malloc() si free(), dar constituie o metoda noua, superioara acestora si adaptata programarii orientate pe obiecte.

Operatorul new poate fi folosit īn urmatoarele situatii:

Exemplu:

int * ip1, *ip2, *ip3;
ip1=new int;      // variabila īntreaga neinitializata
ip2=new int(2);   // variabila īntreaga initializata cu 2
ip3=new int[100]; // tablou de 100 de īntregi

Operatorul new poate fi folosit si la alocarea dinamica a obiectelor, īn cazul acesta permitānd apelarea constructorului clasei.

Operatorul delete este complementarul lui new si īnlocuieste functia free() de dezalocare a memoriei dinamice alocate. Daca exista un destructor pentru obiectul care se dezaloca, acest destructor va fi apelat automat de catre delete.

2.6. Operatorul de rezolutie

In C++ este definit un operator de rezolutie ( :: ), care permite accesul la un identificator cu domeniu fisier, dintr-un bloc īn care acesta nu este vizibil, datorita unei alte declaratii. Principala aplicatie a operatorului este legata de clase si obiecte si va fi prezentata mai tārziu.

Exemplu:

int n=1;
void main(void) {
    int n=2;
    afiseaza(n); // afiseaza 2, valoarea variabilei locale n
    afiseaza(::n); // afiseaza 1,valoarea variabilei globale n
  }

3. Clase

3.1. Tipuri class

In C++ clasa este un concept fundamental, este constructia prin care se definesc noi tipuri de date, prin asocierea unui set de functii la o structura de date.

Definitia unei clase presupune declararea clasei, la care se specifica numele clasei, lista claselor de baza din care e derivata clasa, daca exista, si membrii clasei, atāt membrii de date cāt si functii.

Este posibil controlul accesului atāt la datele membre cāt si la functiile membre ale unei clase. In acest scop, se pot utiliza specificatorii de control al accesului: public, private si protected. Membrii privati(date si functii) sunt accesibili numai functiilor membre si prietene(friend) ale clasei. Un membru public poate fi accesat de orice functie din domeniul de declaratie al clasei. Accesul la membrii protejati este similar celui la membrii privati, dar accesul se poate extinde la functiile membre si prietene ale claselor derivate din clasa respectiva. De exemplu, īn cazul clasei Stiva, sunt ascunse utilizatorului toate detaliile de implementare (membrii de date):

class Stiva{
protected:
	int nmax;  // numarul maxim de elemente
	int *tab;	 // tabloul in care se vor memora elementele 
	int varf;	 // indexul elementului din varf
public:
	Stiva(int);	// constructor
	Stiva();		// constructor implicit
	~Stiva();		// destructor
	BOOL Push(int);
	BOOL Pop(int &);
	BOOL Top(int &);
	BOOL not_vida();
	BOOL not_plina();
};
Definirea functiilor membre se poate face fie ca functii inline, la declararea lor īn cadrul clasei, fie īn exteriorul clasei. Pentru definitiile functiilor membre aflate īn afara declaratiei clasei, este necesara specificarea numelui clasei urmat de operatorul de rezolutie īnaintea numelui functiei. De exemplu, definirea operatiei de adaugare īn stiva se face īn felul urmator:
BOOL Stiva::Push(int ElementNou){
if (not_plina()) {
		tab[++varf]=ElementNou;
		return TRUE;
		}
else return FALSE;
}

Functiile membru pot avea acces la orice membru al clasei respective. Accesul la membrii de date ai instantei curente se face direct, prin numele lor.

Pentru apelul functiilor membre publice sau pentru accesul la datele publice ale unui obiect, din functii care nu sunt membre, se folosesc operatorii de selectie (.) si (->), ca īn cazul structurilor si uniunilor din C.

Exemplu:

        Stiva s1, *ps;
           ...
        ps=new Stiva(20);
           ... 
 	s1.push(5);
	ps->push(8);

3.2. Autoreferinta. Cuvāntul cheie this

Pentru a defini functiile membre sunt necesare referiri la datele membre ale clasei fara a specifica un obiect anume. La apelare, functia este informata asupra identitatii obiectului asupra caruia va actiona prin transferul unui parametru implicit care reprezinta adresa obiectului. De exemplu, īn cazul apelului: s1.Push(3), funtia Push() primeste si adresa stivei s1 īn afara de valoarea 3.

Exista situatii īn care este necesar ca adresa obiectului sa fie utilizata īn definitia functiei. Ne putem referi la acest pointer prin cuvāntul cheie this, declarat implicit īn orice functie si initializat sa indice īnspre obiectul pentru care este invocata functia membru. Principala utilizare a lui this este la scrierea functiilor care manipuleza direct pointeri.

3.3. Membrii statici ai unei clase

Fiecare obiect de tip clasa are propriile lui copii ale tuturor membrilor acelei clase. Este posibila definirea de membrii care sa fie folositi īn comun de catre toate obiectele clasei. Datele statice exista īntr-o singura copie, comuna tuturor obiectelor. Crearea, initializarea si accesul la aceste date statice sunt total independente de obiectele clasei. Functiile membre statice efectueaza operatii care nu sunt asociate obiectelor individuale, ci īntregii clase. Din acest motiv, la apelarea lor nu este obligatorie indicarea unui obiect. Un membru static poate fi referit si indicānd numele clasei si folosind operatorul de rezolutie de domeniu.

3.4. Functii si clase friend

Functiile prietene (friend) pot folosi membrii privati ai unei clase, desi ele īnsele nu sunt membri. Functiile prietene au fost introduse pentru cazurile īn care o functie coopereaza strāns cu o clasa, dar nu face parte din acea clasa. O functie prietena este declarata asemanator cu o functie membru, avānd prototipul īn interiorul clasei, precedat de cuvāntul cheie friend. Exista si posibilitatea ca una sau mai multe functii membre ale unei clase sa fie functii prietene ale altei clase. Se poate declara si o clasa Y prietena a unei clase X, īn acest caz toate functiile clasei Y sunt prietene ale clasei X si au acces nelimitat la membrii privati ai clasei X.

Exemplu:

In clasa Stiva definita anterior, varful este o variabila protejata, care poate fi accesata numai din cadrul functiilor membre ale clasei stiva. Daca se defineste clasa Test, prietena a clasei Stiva, atunci orice functie membra a clasei Test are acces direct nelimitat la variabila varf a unei stive:

  class Stiva {
   .....
   friend class Test;
   };

  class Test {
   .....
   void ofct();
   };

  void Test::ofct() {
    Stiva s;
    ....
    s.varf=5; // devine o operatie permisa, pentru ca Test e clasa prietena !
    ....
    }

3.5. Constructori si destructori

In cazul variabilelor obisnuite, compilatorul asigura alocarea spatiului de memorie si eventual initializarea explicita cu valori initiale īn declaratie. Pentru variabilele dinamice, compilatorul C nu dispune de nici o metoda de initializare si nici operatorul new nu rezolva toate situatiile. In acest caz, ramīne īn grija programatorului atribuirea de valori adecvate datelor īnainte de utilizare. Aceasta abordare este nesatisfacatoare īn multe situatii īn cazul obiectelor.

Pentru crearea, initializarea, copierea si respectiv distrugerea obiectelor, īn C++ se folosesc functii speciale, numite constructori si destructori. Constructorul se apeleaza automat la crearea fiecarui obiect al clasei, static, automatic sau dinamic(cu operatorul new), inclusiv pentru obiecte temporare.

Destructorul este apelat automat la eliminarea unui obiect, la īncheierea timpului sau de viata, sau poate fi solicitat prin program, cu operatorul delete.

Constructorii si destructorii se declara si se definesc similar cu celelalte functii membre, dar se disting de acestea printr-o serie de caracteristici specifice:

Alta categorie de constructor este constructorul de copiere. Necesitatea de copiere a obiectelor intervine cānd un obiect este transferat ca parametru sau rezultat al unei functii, sau la crearea unui obiect temporar. Solutia oferita de C++ consta īn utilizarea unui constructor special, numit constructor de copiere. In absenta unei definitii explicite īn cadrul clasei, compilatorul genereaza automat un constructor de copiere care initializeaza datele noului obiect cu valorile corespunzatoare din obiectul specificat, prin copiere membru cu membru. Aceasta nu este o solutie buna īn cazul īn care clasa contine membrii variabile dinamice si cand e necesara scrierea unui constructor de copiere special.

Declaratia constructorului de copiere pentru o clasa trebuie sa specifice un parametru unic de tipul referinta de obiecte a acelei clase.

De exemplu, definitia unui constructor de copiere adecvat pentru clasa Stiva ar fi:

  Stiva::Stiva(Stiva&s) {
   nmax=s.nmax;
   tab=new int[nmax];
   varf=s.varf;
   for (int i=0; i<=varf; i++) tab[i]=s.tab[i];
   }

3.6. Supraīncarcarea operatorilor

Functiile operator constituie un tip special de functii , care s-ar putea utiliza pentru redefinirea operatorilor de baza care apar īn C. Un tip de clasa se poate defini īmpreuna cu un set de operatori asociati, obtinuti prin supraīncarcarea operatorilor existenti. In acest fel, se efectueaza operatii specifice cu noul tip la fel de simplu ca īn cazul tipurilor standard. Procedeul consta īn definirea unei functii cu numele

	operator < simbol >

De exemplu, se presupune definirea unei clase NrComplex, si se doreste supradefinirea operatorului de adunare, pentru adunarea a doua numere complexe. Functia operator+() trebue sa primeasca doi parametri de tipul NrComplex si sa returneze un rezultat de tip NrComplex, avand acces direct la datele membre ale clasei.

Exista 2 posibilitati de realizare: ca functie prietena a clasei NrComplex sau ca functie membra a clasei NrComplex.

Exemplu - supradefinirea operatorului + ca functie friend:

  class NrComplex {
   float real, imaginar;
   public:
	NrComplex(float r=0, float i=0);
	friend NrComplex operator+(NrComplex, NrComplex); // operatorul suma definit ca
                                                          // functie prietena a clasei
   };

  NrComplex  operator+ (NrComplex c1, NrComplex c2) {
   NrComplex c;
   c.real=c1.real+c2.real;
   c.imaginar=c1.imaginar+c2.imaginar;
   return c;
  }

  void main() {
   NrComplex c1(1,2), c2(5,5), s;
   s=c1+c2;
  }
Expresia c1+c2 este interpretata de compilator ca un apel al functiei
    operator+(c1,c2)

Exemplu - supradefinirea operatorului + ca functie membra:

  class NrComplex {
   float real, imaginar;
   public:
	NrComplex(float r=0, float i=0);
	NrComplex operator+(NrComplex); // operatorul suma definit ca
                                        // functie membra a clasei
   };

  NrComplex  NrComplex::operator+ (NrComplex c2) {
   NrComplex c;
   c.real=real+c2.real;
   c.imaginar=imaginar+c2.imaginar;
   return c;
  }

  void main() {
   NrComplex c1(1,2), c2(5,5), s;
   s=c1+c2;
  }
O functie membra primeste in mod implicit adresa obiectului pentru care este apelata. Functia operator+() va avea prototipul
   NrComplex operator+(NrComplex);
Expresia c1+c2 este interpretata de compilator ca un apel al functiei:
   c1.operator+(c2)
In acest caz, al supradefinirii operatorului + ca functie membru, datorita transferului implicit al primului operand, apare o asimetrie in definitia functiei operator+().

Mai apare si o alta restrictie impusa de functiile operator membre ale clasei: primul operand este intotdeauna de tipul clasa respectiv, deci solutia nu este adecvata daca primul operand trebuie sa fie de un alt tip.

Alte precizari privind supradefinirea operatorilor:

Se pot supradefini in C++ numai operatori existenti, deci simbolul asociat functiei operator trebuie sa fie deja definit ca operator pentru tipurile standard (nu e permisa introducerea unor simboluri noi de operatori). De asemenea, nu se pot modifica pluralitatea (un operator unar nu poate fi supradefinit ca unul binar sau invers), precedenta, asociativitatea.

Functia operator trebuie sa aiba cel putin un parametru (implicit sau explicit) de tipul clasa caruia ii este asociat operatorul respectiv. Aceasta restrictie implica faptul ca supradefinirea operatorilor e posibila numai pentru tipurile clasa definite in program, pentru tipurile standard operatorii isi pastreaza definitia.

Functiile operator pot fi implementate ca si functii membre a clasei sau ca si functii prietene a clasei. In particular, pentru operatorii: = [] () -> functia operator trebuie sa fie membra a clasei.

In general, pentru un operator binar op, expresia x op y este interpretata:

   x.operator op(y); // functie membru
sau
   operator op(x,y); // functie friend

In general, pentru un operator unar op, expresiile x op sau op x se interpreteaza:

   x.operator op(); // functie membru
sau
   operator op(x); // functie friend