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.
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; }
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.
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();
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();
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:
#includemain() { int c; cin >> c; cout << c; }
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; }
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 templateUn exemplu de program care utilizeaza trei stive, o stiva de numere întregi, una de caractere si una de numere complexe este prezentat în continuare.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); }
// Fisierul ProgStT.cpp #include "Stiva_T.hpp" #includetypedef 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.