Etichete: POO

Generalități

În programarea procedurală, un program era alcătuit din nouă componente distincte: date și funcții; funcțiile prelucrează datele la care au acces, prin intermediul parametrilor sau în alt mod.

Programarea orientată obiect – POO (sau Object Oriented Programming – OOP) este o modalitate de proiectare a programelor în care datele prelucrate și operațiile cu acestea sunt încapsulate în aceeași structură, numită obiect. Funcțiile care fac parte dintr-un obiect au acces la datele care caracterizează acel obiect, iar un program este alcătuit din mai multe obiecte, care interacționează.

Programarea orientată pe obiecte se bazează pe următoarele principii fundamentale:

  • încapsulare = mecanismul prin care atât datele, cât și funcțiile sunt plasate în aceeași structură, numită clasă și stabilirea nivelului de acces la conținutul acesteia;
  • abstractizare = identificarea datelor și funcțiilor relevante pentru o anumită clasă;
  • moștenire = proprietatea claselor de a prelua date și metode ale unor clase definite anterior. Clasa inițială se numește clasă de bază, iar cea nouă se numește clasă derivate;
  • polimorfism = posibilitatea ca atât clasa de bază, cât și clasa derivată să conțină metode cu același nume, dar diferite ca funcționalitate.

Noțiunile de clasă și obiect

Clasa este un tip de date, similar cu struct, care conține atât câmpuri de tip dată (numite proprietăți), cât și câmpuri de tip funcție (numite și metode). Câmpurile clasei (proprietăți sau metode) se mai numesc membri ai clasei. Astfel avem, date membre și funcții membre.

Obiectul reprezintă o dată de tipul clasei – de exemplu o variabilă. Declararea unei variabile de acest tip se mai numește instanțiere a clasei. Spunem că obiectul este o instanță a clasei.

Pentru membrii unei clase (date sau metode) se precizează anumite caracteristici, numite modificatori (specificatori) de acces, care stabilesc cum se face accesul la acestea:

  • private – interzic accesul la date și metode în afara clasei;
  • public – permit accesul la date și metode din afara clasei;
  • protected – interzic accesul din afara clasei, dar îl permit din clasele derivate – are sens în contextul derivării claselor.

Exemplu

#include <iostream>

using namespace std;

class Fractie{
    private:
        int numarator, numitor;
    public:
		void afiseaza(){
			cout << numarator << "/" << numitor << " ";
		}
		void seteaza(int , int);
};

void Fractie::seteaza(int a , int b){
    numarator = a , numitor = b;
}

int main(){
	Fractie F;
	F.seteaza(3 , 4);
	F.afiseaza();
	/// F.numarator = 7; //eroare, data este privata

    return 0;
}

În exemplul de mai sus:

  • Fractie este numele clasei;
  • F este numele obiectului – instanță a clasei;
  • clasa are două date membre: numarator și numitor. Ele sunt private – nu pot fi accesate din exteriorul clasei;
  • clasa are două funcții membre: seteaza(), care dă valori datelor membre și afiseaza(), care afișează datele membre în forma specifită unei fracții ordinale. Ele sunt publice.

Observații

  • clasele se definesc similar cu structurile
    class Fractie{
    	/// …
    };

    De fapt, diferențele dintre clase și structuri sunt foarte mici. În acest moment putem observa că accesul la câmpurile unei structuri este implicit public, iar accesul la membrii unei clase este implicit privat.
  • accesul la membri clasei, indiferent dacă sunt date sau funcții, se face asemănător cu cel al câmpurilor unei structuri, prin operatorul de acces direct . (punctul), sau, în cazul pointerilor la clase, prin operatorul de acces indirect ->.
    Fractie X;
    X.afiseaza();
  • obiectele unei clase se declara la fel cum se declară variabilele de orice tip
    Fractie x, y; /// două obiecte
    Fractie V10; /// un tablou cu 10 elemente de tip obiect
    Fracte * p; /// pointer la obiect. Obiectul încă nu există!!

    Crearea obiectului se numește instanțiere a clasei, iar obiectl este o instanță a clasei.
  • la fel ca în cazul structurilor, având două obiecte ale aceleiași clase, se pot realiza atribuiri. Trebuie însă știut că este posibil ca rezultatul atribuirii să nu fie cel așteptat!
    Fractie x , y;
    x.seteaza(3 , 4);
    y = x;
    y.afiseaza();
  • putem declara pointeri la obiecte. Obiectele adresate de pointeri pot fi statice sau dinamice, iar accesul la datele și funcțiile membre se face prin intermediul operatorului de acces indirect ->
    Fractie x , * p;
    x.seteaza(3 , 4);
    p = & x;
    p->afiseaza();
  • metodele unei clase pot fi descrise în modul următor:
    • plasăm în interiorul clasei definiția metodei, obținând o metodă inline – în exemplul de mai sus metoda afiseaza() a fost scrisă în acest mod
    • plasăm în interiorul clasei numai declarația (prototipul) metodei, iar definiția o plasăm în exteriorul clasei. În exemplul de mai sus am scris în acest mod metoda seteaza(). Pentru a preciza că este vorba despre o metodă a clasei și nu o funcție oarecare se folosește operatorul de rezoluție ::
      void Fractie::seteaza(int a , int b){
          numarator = a , numitor = b;
      }
    • un avantaj al metodelor inline este că orice apel al ei este înlocuit cu secvanța de instrucțiuni corespunzătoare. Această abordare conduce la economie de timp, dar și la un necesar mai mare de memorie.
Principiul încapsulării recomandă să fie publice numai anumite metode ale clasei, prin care să se poată modifica starea obiectului – valorile proprietăților sale, iar acestea din urmă să fie private. Tot private vor fi și metodele care sunt necesarea pentru gestionarea proprietăților obiectului, dar care nu trebuie apelate din exteriorul clasei.

Constructori

Constructorul reprezintă un mecanism prin care datele membre ale unui obiect dintr-o clasă primesc valori la instanțierea (declararea/crearea) obiectului.

  • constructorii sunt metode (funcții membre) ale clasei și se apelează la instanțierea obiectului
  • constructorul:
    • este o metodă a clasei
    • are același nume cu clasa
    • este o funcție fără tip; la declarare/definire în locul tipului funcție nu se scrie nimic, nici măcar void
  • o clasă poate avea mai mulți constructori, care diferă prin numărul și tipul parametrilor
  • constructorii sunt funcții; blocul lor poate conține orice fel de instrucțiuni care reflectă logica clasei pe care o definim!

Exemplu

class Fractie{
    private:
        int numarator, numitor;
        void simplifica(){
        	int a = numarator, b = numitor , r;
            while(b)
            	r = a % b, a = b, b = r;
            numarator /= a, numitor /= a;
        }
    public:
		void afiseaza(){
			cout << numarator << "/" << numitor << endl;
		}
		void seteaza(int a , int b){
            numarator = a , numitor = b;
        }
		Fractie(){
			numarator = 0, numitor = 1;
		}
		Fractie(int a){
			numarator = a, numitor = 1;
		}
		Fractie(int a , int b){
			if(b == 0)
				b = 1;
			numarator = a, numitor = b;
            simplifica();
		}
};

int main(){
	Fractie x;
	x.afiseaza();
	Fractie y(3);
	y.afiseaza();
	Fractie z(3 , 2);
	z.afiseaza();
	return 0;
}

Observații

  • în lipsa unui constructor definit de programator, se va apela constructorul implicit – el permite declararea obiectelor, alocând memorie pentru ele;
  • dacă o clasă are constructor cu parametri, constructorul implict nu mai există. În consecință, nu se pot declara obiecte fără parametri, ceea ce este de multe ori necesar. De aceea, dacă o clasă are constructor, este necesar să aibă și constructor fără parametri, prin care un datele membre ale obiectului declarat să primească valorile pe care le consideram implicite.

Constructor de copiere

Constructorul de copiere creează un obiect inițilizândul cu un alt obiect, din aceeași clasă, care există deja. El se folosește pentru:

  • inițializarea unui obiect cu un alt obiect
  • transmiterea prin valoare a unui obiect ca parametru pentru o funcție
  • copierea unui obiect returnat de o funcție

Clasele au un constructor de copiere implicit, care realizează copierea bit-cu-bit a conținutului obiectului. Acest mod de copiere nu este corect atunci când obiectul conține date alocate dinamic, caz în care este mecesară definirea unui constructor de copiere explicit.

Sintaxa este:

NumeClasa (const NumeClasa &); 

Exemplu:

Fractie(const Fractie & F)
{
	numarator = F.numarator();
    numitor = F.numitor();
    simplifica();
}

Destructor

Destructorul este o metodă publică care se apelează la eliminarea din memorie a unui obiect. O clasă poate avea un singur destructor, iar numele lui este identic cu al metodei, dar precedat de caracterul ~. Destructorul, similar cu destructorul, este o funcție fără tip. Destructorul nu are parametri.

Exemplu

class Fractie{
    private:
        int numarator, numitor;
    public:
		void afiseaza(){
			cout << numarator << "/" << numitor << endl;
		}
		...
		~Fractie(){
			cout << "Fractia " << numarator << "/" << numitor << " a fost eliminata." << endl;
		}
};

int main(){
	Fractie x(1 , 4);
	x.afiseaza();

	return 0;
}

Regula celor Trei

Dacă o clasă are nevoie de unul dintre următoarele:
  • un constructor de copiere
  • operator de atribuire
  • destructor
atunci, foarte probabil, are nevoie de toate trei!

Funcții prietene

Uneori este necesar ca din interiorul unor funcții care nu sunt metode ale unui obiect să accesăm membrii privați ai acestuia. Acest lucru poate fi realizat prin intermediul funcțiilor prietene. Ele:

  • sunt declarate în interiorul clasei; prototipul lor este precedat de cuvântul cheie friend
  • funcția prieten are un parametru de tipul clasei; în funcție se pot accesa membrii privați ai acesteia
  • funcțiile prietene nu sunt metode ale clasei; ele nu pot accesa direct membrii clasei, ci numai membrii parametrului de tipul clasei

Exemplu:

class Fractie{
    private:
        int numarator, numitor;
    public:
		...
		friend void Afiseaza(Fractie F);
};

void Afiseaza(Fractie F)
{
	cout << F.numarator << "/" << F.numitor << endl;
}

int main(){
	Fractie x(1 , 4);
	Afiseaza(x);

	return 0;
}

Cuvântul cheie this

În definiția metodelor unei clase, cuvântul cheie this reprezintă un pointer către obiectul curent.

Este necesar în următoarele situații:

  • parametrii unor metode au același nume cu datele membre. Exemplu: clasa fracție conține câmpul numarator și o metodă are un parametru cu același nume, atunci parametrul poate fi referit prin identificatorul numarator, iar câmpul prin expresia this->numarator sau (*this).numarator;
  • uneori este necesar ca o metodă să returneze obiectul curent, sau o referință la obiectul curent;

Exemplu:

class Fractie{
    private:
        int numarator, numitor;
    public:
		...
		Fractie & creste(int n)
		{
			this->numarator += n * this->numitor; // aici this nu este necesar
			return * this;
		}
		friend void Afiseaza(Fractie F);
};

void Afiseaza(Fractie F)
{
	cout << F.numarator << "/" << F.numitor << endl;
}

int main(){
	Fractie x(1 , 4);
	Afiseaza(x);
	x.creste(2).creste(3);
	Afiseaza(x);
	return 0;
}

Un exemplu complet

Programul de mai jos implementează clasa Fractie și reprezintă o soluție pentru problema #spfractii :

  • respectă principiul încapsulării;
  • conține:
    • proprietăți (private);
    • metode private;
    • metode publice;
    • constructor;
    • constructor de copiere;
    • funcții prietene;
    • this. Prin intermediul său, metodele vor returna obiectul curent, fapt ce permite înlănțuirea acestora – vezi apelul funcției Produs().
#include <iostream>

using namespace std;

class Fractie{
	private:
		int numarator, numitor;	// proprietăți
		void simplifica();		// metodă privată
	public:
		Fractie(int _numarator = 0, int _numitor = 1); // constructor
		Fractie(const Fractie &);	// constructor de copiere
		Fractie & citeste();	//	metodă publică
		Fractie & scrie();		// metodă publică
		friend Fractie Suma(Fractie , Fractie);	// functie prietena
		friend Fractie Produs(Fractie , Fractie);	// functie prietena
};

Fractie::Fractie(const Fractie & F)
{	// constructor de copiere
	numarator = F.numarator;
	numitor = F.numitor;
	simplifica();
}

Fractie::Fractie(int _numarator /* = 0 */, int _numitor /* = 1 */)
{	// constructor
	numitor = _numitor, numarator = _numarator;
	simplifica();
}
Fractie & Fractie::citeste()
{
	// citeste numaratorul si numitorul obiectului curent
	// returnează obiectul curent
	cin >> numarator >> numitor;
	return * this;
}

Fractie & Fractie::scrie()
{
	// afisează numaratorul si numitorul obiectului curent
	// returnează obiectul curent
	cout << numarator << " " << numitor <<endl;
	return * this;
}

void Fractie::simplifica()
{	//metoda privata; realizeaza simplificarea fractiei merorate în obiectul curent
	int a = numarator, b = numitor;
	while(b)
	{
		int r = a % b;
		a = b;
		b = r;
	}
	numarator /= a;
	numitor /= a;
}

Fractie Suma(Fractie F, Fractie G)
{
	// funcție prietenă. Putem accesa datele private ale obiectelor F , G
	int x = F.numarator * G.numitor + F.numitor * G.numarator,
		y = F.numitor * G.numitor;
	Fractie Rez(x , y); // s-a apelat constructorul pentru crearea obiectului. Aici s-a făcut simplificarea
	return Rez; // s-a apelat costructorul de copiere
}

Fractie Produs(Fractie F, Fractie G)
{	
	// funcție prietenă. Putem accesa datele private ale obiectelor F , G
	int x = F.numarator * G.numarator,
		y = F.numitor * G.numitor;
	return Fractie(x , y); // se apelează constructorul de copiere. Aici se face simplificarea
}

int main()
{
	Fractie A , B;
	A.citeste(), B.citeste();
	Fractie S = Suma(A , B);
	S.scrie();
	Produs(A , B).scrie();
	return 0;
}