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

4. Conceptul de mostenire. Clase derivate.

4.1. Declararea claselor derivate. Controlul accesului

Procedeul de derivare permite definirea unei clase noi, numita clasa derivata, care mosteneste proprietatile unei clase deja definite, numita clasa de baza, pe care le completeaza cu proprietati noi. Clasa derivata contine membrii clasei de baza, la care se adauga membri suplimentari, specifici (date si functii). Dintr-o clasa de baza pot fi derivate mai multe clase si fiecare clasa derivata poate servi ca baza pentru alte clase derivate. Se poate realiza o ierarhie de clase, care sa modeleze în mod adecvat sisteme complexe. Este posibil ca o clasa sa mosteneasca proprietatile mai multor clase de baza (mostenire multipla).

Exemplu: Un sistem simplu pentru evidenta populatiei memoreaza date despre diverse categorii de persoane. Fiecare persoana este caracterizata prin nume, adresa, si eventual alte informatii, specifice categoriei din care face parte. Se presupun doua categorii, elev si salariat. Fiecare elev este caracterizat de clasa în care este, fiecare salariat de denumirea institutiei unde lucreaza. Se poate defini urmatoarea ierarhie de clase:

class Persoana {
protected:
	char *nume;
	char *adresa;
public:
	Persoana(char *N, char *A);
	~Persoana();
	char * Nume();
	char *AdresaActuala();
	void SchimbaAdresa(char *A);
	void AfiseazaDate();
};

class Elev: public Persoana {
protected:
float media;
public:
	Elev(char *N, char *A, int cl);
	~Elev();
	int Clasa();
void ActualizeazaClasa(int cl);
};

class Salariat: public Persoana {
protected :
char * loc_munca;
public:
Salariat(char *N, char *A, char *LM);
~Salariat();
char *LocMunca();
void SchimbaLocMunca(char *LM);
};
Pentru a putea controla drepturile de acces ale clasei derivate asupra membrilor clasei de baza, declaratia poate contine specificatori de control al accesului. Derivarea poate fi de tip public sau private. Membrii privati ai clasei de baza nu pot fi accesati nicicând din cea derivata. Membrii protejati pot fi accesati din cadrul clasei derivate numai daca mostenirea este publica (cazul din exemplul anterior). Accesul din exterior asupra membrilor clasei derivate care sunt mosteniti din membri publici ai clasei de baza ramâne posibil numai daca mostenirea a fost publica.

4.2. Constructori si destructori pentru clasa derivata

Regulile de functionare ale constructorilor si destructorilor ramân valabile si în cazul claselor derivate, tinând seama însa de aspectul particular al mostenirii datelor clasei de baza: pentru construirea unui obiect al clasei derivate, se începe prin crearea unui obiect al clasei de baza si se apeleaza constructorul clasei de baza. Apoi se creaza elementele adaugate de clasa derivata si se apeleaza constructorul clasei derivate. La eliminarea unui obiect al clasei derivate, este apelat întâi destructorul clasei derivate, apoi cel al clasei de baza.

Un exemplu de definire a constructorului clasei Elev, derivata din clasa Persoana:

Elev:: Elev(char *N, char *A, int cl) : Persoana(N,A) {
clasa=cl;
}

4.3. Redefinirea functiilor membre

Clasele Elev si Salariat au acces la functia de afisare a clasei Persoana. Totusi, aceasta afiseaza numai datele comune tuturor persoanelor, fara sa poata afisa, daca persoana este un elev, clasa în care e, sau daca e salariat, unde lucreaza. Pentru a putea face afisarea diferentiata, se poate redefini functia AfiseazaDate ca functie membru în clasele Elev si Salariat. Compilatorul C++ poate distinge functile membre pe baza tipului obiectului apelant.

Definitiile noi din clasa derivata nu înlocuiesc definitiile din clasa de baza, ci se adauga lor. Clasa derivata are la dispozitie si definitiile din clasa de baza, pe care le poate specifica utilizând operatorul de rezolutie.

4.4.Compatibilitatea între o clasa derivata si clasa de baza

Deoarece orice clasa derivata mosteneste proprietatile clasei de baza, se poate vorbi de o oarecare compatibilitate între tipul clasei derivate si tipul clasei de baza. (In exemplul anterior, orice elev este o persoana, dar nu orice persoana este un elev).

Compatibilitatea se manifesta sub forma unor conversii explicite de tip, dintr-un obiect derivat într-un obiect de baza, sau dintr-un pointer sau referinta la clasa derivata într-un pointer la clasa de baza.

Exemplu: conversii permise sunt cele din exemplul urmator:

	Persoana p1, *pp1, *pp2;
	Elev e1;
	p1=e1;
	pp1=new Elev("Ion","str. Mare", 3);
	pp2=new Salariat("Vasile","str. XXX", "ABC S.A");
	pp1->AfiseazaDate();

4.5. Functii virtuale

Decizia asupra versiunii functiei care se apeleaza este luata în momentul compilarii, numita legare statica (early binding). Pe de alta parte, tipul obiectului adresat de un pointer este determinat de declaratie si fixat tot în momentul compilarii. S-a aratat în paragraful precedent ca este posibil ca un pointer al unui tip clasa de baza sa contina adresa unui obiect apartinând unei clase derivate. La utilizarea variabilei pointer , în cazul legarii statice, nu se tine seama de tipul obiectului adresat, si functia membra selectata este cea definita în clasa de baza. De exemplu, în secventa anterioara, apelul

	pp1->AfiseazaDate(); 
determina apelarea functiei membru a clasei Persoana, desi obiectul în cauza este un salariat si functia a fost redefinita la nivelul clasei Salariat. Aceasta reduce utilitatea redefinirii functiilor membre care s-a facut.

Pentru a se putea urmari tipul obiectului adresat de pointer la un moment oarecare, este necesara identificarea versiunii functiei care trebuie apelata în timpul executiei programului. Acest mod de lucru se numeste legare dinamica (late binding). Functiile membre pentru care se realizeaza legatura dinamica se numesc functii virtuale si se declara cu ajutorul cuvântului cheie virtual. De exemplu, declaratia functiei AfiseazaDate, se va modifica astfel în cadrul clasei Persoana:

    virtual void AfiseazaDate();

5.Operatii de intrare-iesire în C++

5.1. Clasele istream, ostream, iostream

Limbajul C++, ca si limbajul C de altfel, nu contine instructiuni specifice pentru operatiile de intrare-iesire. Acestea se efectueaza cu ajutorul unor proceduri de biblioteca si constituie o aplicare a conceptelor generale ale limbajului: programare orientata pe obiecte, cu ierarhie de clase, supraîncarcarea operatorilor.

Modelul fundamental pentru constructia bibliotecii de intrare-iesire este cel de sir (stream), vazut ca un flux de date de la o anumita sursa la o anumita destinatie.

Exista doua ierarhii de clase, având ca baze clasele streambuf si ios.

Ierarhia streambuf realizeaza gestionarea tampoanelor de memorie utilizate în efectuarea operatiilor de intrare- iesire.

Ierarhia ios gestioneaza toate operatiile de intrare-iesire si pune la dispozitie o interfata de nivel înalt pentru programator.

Principalele categorii de clase sunt:

In sistemul de intrare-iesire C++ sunt prevazute dispozitive logice predefinite: cin (intrare consola), cout(iesire consola), cerr (iesire ptr erori).

Clasele istream si ostream dispun de seturi de functii de intrare/iesire si supradefinesc cei 2 operatori de deplasare bit cu bit (shift) pentru a efectua transferul de informatie cu un stream si formatarea pentru tipurile de baza:

In clasa ostream, operatorul << este supradefinit sub forma:

    ostream& operator << (tip_de_baza);

Operatorul admite ca operanzi o expresie de un tip fundamental oarecare si, in mod implicit, adresa obiectului din clasa ostream specificat la apelare. Rolul sau este de a transfera valoarea expresiei, cu un format adecvat, catre acel stream. Operatorul returneaza ca rezultat adresa dispozitivului pentru a permite operatii de iesire multiple, inlantuite.

In clasa istream, operatorul >> este supradefinit sub forma:

   istream& operator >> (tip_de_baza);

Exemplu:

  #include 
  main() {
  int c;
  cin >> c;
  cout << c;
  }

5.2. Supraincarcarea operatorilor << si >> pentru tipuri de date definite de programator

Este posibila supradefinirea operatorilor de inserare/extragere din stream pentru a permite utilizarea lor si pentru tipuri de date definite de programator, pe linga cele standard.

Declaratiile operatorilor pot fi de forma:

    istream& operator>> (istream&, tip_utilizator&);
    ostream& operator<< (ostream&, tip_utilizator);

Primul argument trebuie sa fie o referinta la un obiect istream sau ostream, sau al uneia din clasele derivate acestora. De aceea, functiile operator nu pot fi membre ale clasei definite de utilizator. De regula sunt functii independente prietene ale clasei definite de utilizator. Rezultatul returnat trebuie sa fie adresa obiectului stream primit ca argument, pentru a permite efectuarea unei secvente de operatii.

Exemplu:

   class NrComplex {
      float re, im;
     public:
      NrComplex(float r=0, float i=0) {
         re=r; im=i;
         }
      friend istream& operator >> (istream&, NrComplex&);
      friend ostream& operator << (ostream&, NrComplex);    
     };

   istream& operator >> (istream& in, NrComplex& c) {
     in>> c.re >> c.im;
     return in; 
   }
   
   ostream& operator << (ostream& out, NrComplex c){
     out << "Re=" << c.re << "Im=" << c.im;
     return out;
   }   

6. Tipuri parametrizate (template)

S-a prezentat anterior un exemplu de implementare a clasei Stiva, ca fiind o stiva de intregi. În mod tipic, programatorii vor avea nevoie de stive de elemente apartinând unor tipuri diferite. S-ar putea sa apara necesitatea definirii unor tipuri ca: StivaDeIntregi, StivaDeCaractere, StivaDeNrComplexe, StivaDeFiguriGeometr, etc.. Toate acestea ar implica scrierea câte unei clase stiva pentru fiecare tip de elemente continute. Este probabil ca cel care scrie tipul Stiva sa nu cunoasca toate tipurile de elemente pe care ulterior, alti programatori vor dori sa le introduca în stive. Oricum, scrierea unui numar mare de asemenea stive (o familie de stive) ar fi total ineficienta în ceea ce priveste reutilizarea codului.

Solutia la aceasta problema ar fi posibilitatea ca tipul Stiva sa fie exprimat astfel încât sa primeasca drept parametru tipul elementelor.

În C++, exista posibilitatea de a crea familii de functii sau de clase cu ajutorul sabloanelor (template).

Se poate reconsidera acum exemplul stivei.

// Fisierul  Stiva_T.hpp

#define BOOL int
#define TRUE 1
#define FALSE 0

template  class Stiva {
protected:
	int nmax; 		// numarul maxim de elemente
	TipElement *tab;	// tabloul in care se vor memora elementele
	int varf;			// indexul elementului din varf

public:
	Stiva(int n=100);
	~Stiva();
	BOOL Push(TipElement);
	BOOL Pop(TipElement &);
	BOOL Top(TipElement &);
	BOOL not_vida();
	BOOL not_plina();
};

template  
Stiva::Stiva(int n) {
	nmax=n;
	tab=new TipElement[nmax];
	varf=-1;
	}


template  
Stiva::~Stiva() {
	delete tab;
	}

template 
BOOL Stiva::Push(TipElement ElementNou){
if (not_plina()) {
		tab[++varf]=ElementNou;
		return TRUE;
		}
else return FALSE;
}

template 
BOOL Stiva::Pop(TipElement &ElementVarf) {
if (not_vida()) {
		ElementVarf=tab[varf--];
		return TRUE;
		}
else return FALSE;
}

template 
BOOL Stiva::Top(TipElement &ElementVarf) {
if (not_vida()) {
		ElementVarf=tab[varf];
		return TRUE;
		}
else return FALSE;
}

template  
BOOL Stiva::not_plina(){
return (varf 
BOOL Stiva::not_vida() {
return (varf>=0);
}
Un exemplu de program care utilizeaza trei stive, o stiva de numere întregi, una de caractere si una de numere complexe este prezentat în continuare.
// Fisierul ProgStT.cpp

#include "Stiva_T.hpp"
#include 

typedef   struct c { int x,y;} complex;
// crearea unor noi tipuri
// pentru instantierea sabloanelor
typedef   Stiva StivaInt;	
typedef   Stiva StivaChar;	
typedef   Stiva StivaComplex;

void main(void){
StivaInt s1(10);	// instantierea obiectelor
StivaChar s2;
StivaComplex s3;

int e1;
char e2;
complex e3={1,3};

s1.Push(5); s1.Pop(e1);
s2.Push('a');  s2.Pop(e2);
e3.x=1; e3.y=2;
s3.Push(e3); s2.Pop(e3);
cout<<"e1="<< e1 << "e2=" << e2 << "e3=" << e3.x << " " << e3.y <<"\n";
}

Definitia unui sablon de clasa nu este nici clasa, nici obiect. Sablonul unei clase este o descriere a modului în care compilatorul va genera o noua clasa, pornind de la tipurile date ca parametru. În C++, o clasa este inutila daca nu se declara o instanta (obiect) a clasei respective. La fel, un sablon este inutil daca nu se declara o instanta (clasa) a sablonului respectiv.

Un alt aspect legat de folosirea sabloanelor este legat de faptul ca ele pot fi folosite numai cu tipuri care accepta operatiile necesare sablonului. În exemplul cu stiva, este necesar ca tipul elementelor stivei sa accepte operatia de atribuire.

Se observa faptul ca în fisierul antet Stiva_T.hpp este inclusa atât declaratia cât si definitia sablonului Stiva, pentru a permite compilatorului sa genereze clasele necesare (instantele sablonului) fiecarui modul utilizator care foloseste clasa generica Stiva.