This is an old revision of the document!


Laborator 03 - Particularitățile clasei

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • lucreze cu clase care includ membri de tip pointer, gestionând corect alocarea și eliberarea memoriei
  • recunoască și să utilizeze membri constanți, garantând imutabilitatea acestora
  • folosească atribute și funcții statice, care aparțin clasei în ansamblu și nu instanțelor individuale
  • implementeze destructorul pentru a asigura eliberarea resurselor la distrugerea obiectelor
  • implementeze constructorul de copiere și operatorul de atribuire, gestionând corect copierea profundă a conținutului obiectelor

Introducere

În cadrul laboratorului anterior ne-am axat pe scrierea corectă a unei clase respectând principiul încapsulării datelor. În acest laborator atenția ne este îndreptată tot asupra modului în care o clasă este scrisă în așa fel încât să respecte principiile OOP, dar vom introduce noi tipuri de membri și de asemenea vom prezenta câteva din funcțiile membre care sunt specifice clasei.

Pentru a înțelege despre ce vom vorbi pe parcursul laboratorului propunem ca și clasă pentru exemplificare clasa Conifer căreia îi vom adăuga noutățile rând pe rând. De asemenea, pentru această clasă vom pune la dispoziție constructori și accesori de tip get și set.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime);
 
	double getPret() const;
	float getInaltime() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
};

Iar implementările metodelor le vom regăsi în fișierul “Conifer.cpp” după cum urmează.

#include "Conifer.h"
 
Conifer::Conifer()
{
	pret = 0.0;
	inaltime = 0.0f;
}
 
Conifer::Conifer(const double& pret, const float& inaltime)
{
	this->pret = pret;
	this->inaltime = inaltime;
}
 
double Conifer::getPret() const
{
	return pret;
}
 
float Conifer::getInaltime() const
{
	return inaltime;
}
 
void Conifer::setPret(const double& pret)
{
	if (pret <= 0)
	{
		return;
	}
 
	this->pret = pret;
}
 
void Conifer::setInaltime(const float& inaltime)
{
	if (inaltime <= 0)
	{
		return;
	}
 
	this->inaltime = inaltime;
}

Până în acest punct am făcut doar o scurtă recapitulare a ceea ce am învățat în laboratorul precedent.

Membri de tip pointer

Să presupunem că suntem administratorii unui site care aparține unei firme care se ocupă de comercializarea coniferelor. Firma respectivă are mai multe tipuri de conifere care sunt caracterizate prin denumire, spre exemplu: brad, pin, molid, zadă, ienupăr, etc. După cum se poate observa denumirea fiecărui tip de conifer variază. Noi ca și administratori ai site-ului ar trebui să concepem un mecanism prin care să putem stoca în baza de date aceste denumiri indiferent de varietatea lor.

Dacă ne uităm acum mai atent la denumirile enumerate anterior putem observa că din punct de vedere al programării acestea nu sunt altceva decât șiruri de caractere care au lungimi diferite. Pentru a putea accepta orice fel de denumire de conifer în cod ar trebui ca în clasa noastră să avem un membru în care putem stoca șiruri de caractere.

Să adăugăm în clasa noastră un atribut care ne va ajuta să stocăm denumirea pentru fiecare conifer în parte.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire; // membru de tip pointer la char
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire); // se aduga un nou parametru la acest constructor
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
};

Am adăugat un atribut denumire, de tip pointer la char, pentru a putea gestiona denumirile coniferelor într-un mod mai flexibil. Am optat pentru un pointer deoarece intenționăm să alocăm memoria dinamic. De ce ar trebui să alegem această abordare? Pentru că lungimea fiecărei denumiri poate varia, iar folosirea unei dimensiuni fixe ar putea fi ori insuficientă, ori ar putea risipi spațiu.

Alocarea dinamică ne oferă flexibilitatea de a rezerva exact câtă memorie este necesară, fără a irosi resurse sau risca să depășim limitele. Acest lucru ne permite să gestionăm eficient memoria și să adaptăm programul la nevoile reale.

Shallow Copy

Copia superficială (Shallow Copy) apare atunci când un obiect este copiat fără a se crea duplicatul complet al datelor sale. În cazul în care obiectul conține pointeri, o copie superficială va reține doar adresa de memorie stocată în pointeri, nu și datele efective la care aceștia pointează. Astfel, atât obiectul original, cât și copia vor partaja aceeași zonă de memorie pentru acele date.

Acest tip de copiere poate duce la probleme neașteptate, cum ar fi modificarea datelor originale prin intermediul copiei sau dezalocarea incorectă a memoriei, deoarece două obiecte diferite vor încerca să gestioneze aceeași resursă. De aceea, în astfel de situații este recomandată utilizarea copiei profunde (Deep Copy), care creează o duplicare completă a datelor, inclusiv a zonelor de memorie la care pointerii fac referire.

Pentru a înțelege conceptul de copie superficială vom urmări exemplul de cod de mai jos.

#include <iostream>
#include <cstring>
 
int main()
{
	char* sir1 = new char[strlen("Ionescu") + 1];
	strcpy(sir1, "Ionescu");
 
	char* sir2 = sir1; // shallow copy (sir2 partajeaza aceeasi zona de memorie ca sir1)
	sir2[0] = 'J';
 
	std::cout << "Primul sir de caractere are valoarea: " << sir1 << '\n';
	std::cout << "Al doilea sir de caractere are valoarea: " << sir2 << '\n';
 
        delete[] sir1;
        /*delete[] sir2; // incorect deoarece deja am eliberat zona de memorie cu o linie mai sus*/
 
	return 0;
}

Pentru a fi și mai clar ce s-a întâmplat pe linia char* sir2 = sir1; vom ilustra grafic după cum urmează.

După cum se poate observa în imaginea de mai sus variabila sir1 conține adresa primului element din vectorul de caractere (adresa caracterului 'I'). Când am facut atribuirea char* sir2 = sir1; am făcut ca pointerul sir2 să pointeze către adresa de pointare a lui sir1. Astfel orice modificare pe care o facem asupra lui sir1 se va răsfrânge asupra lui sir2, valabil și invers după cum se poate observa pe linia unde am schimbat valoarea primului caracter prin intermediul lui sir2. Cu alte cuvinte pointerii sir1 și respectiv sir2 partajează aceeași zonă de memorie.

Exemplul de mai sus ilustrează conceptul de shallow copy. Este important de reținut faptul că acest tip de copiere apare în principal atunci când lucrăm cu pointeri. În cazul tipurilor de date care nu sunt pointeri, copierea se face bit cu bit, ceea ce înseamnă că se realizează automat o copie profundă. Compilatorul se ocupă de acest proces fără a fi nevoie de intervenția noastră.

Deep Copy

Spre deosebire de shallow copy, unde doar adresele de memorie sunt copiate, în deep copy se creează o replică completă a datelor. Deși pare mai costisitor și mai greu de realizat, acest tip de copiere este soluția cea mai bună atunci când ne dorim ca originalul și copia sa să nu partajeze aceeași zonă de memorie. Practic prin copiere în profunzime realizăm o clonă pentru original în adevăratul sens al cuvântului.

Pentru a înțelege cum putem realiza deep copy vom modifica exemplul anterior în așa fel încât variabilele sir1 și sir2 să fie independente, adică să pointeze către adrese diferite în memoria heap.

#include <iostream>
#include <cstring>
 
int main()
{
	char* sir1 = new char[strlen("Ionescu") + 1];
	strcpy(sir1, "Ionescu");
 
	char* sir2 = new char[strlen(sir1) + 1];
	strcpy(sir2, sir1);
 
	sir2[0] = 'J';
 
	std::cout << "Primul sir de caractere are valoarea: " << sir1 << '\n';
	std::cout << "Al doilea sir de caractere are valoarea: " << sir2 << '\n';
 
	delete[] sir1;
	delete[] sir2; // corect deoarece sir1 si sir2 pointeaza catre doua blocuri de memorie diferite
 
	return 0;
}

Iar ca și ilustrare grafică lucrurile arată în felul următor.

Pașii pe care i-am aplicat pentru a realiza deep copy au fost alocarea unui nou spațiu de memorie pentru sir2 și copierea caracterelor lui sir1 în sir2 cu ajutorul funcției strcpy. Astfel atunci când am schimbat valoarea primului caracter din sir2 modificarea nu s-a propagat și pe sir1, deoarece acum cei doi pointeri nu mai partajează aceeași zonă de memorie.

În POO se inteționează ca atunci când se fac copieri acestea să fie în profunzime (deep copy). Copierea superficială poate fi folosită doar dacă suntem siguri că nu avem nevoie de clone reale ci doar de unele false pentru a realiza diverse modificări care să se răsfrângă asupra originalului.

Având acum cele două noțiuni clarificate putem reveni la clasa Conifer pentru a putea implementa constructorii și metodele accesor. Vom face deep copy, deoarece vrem ca fiecare obiect să aibă pentru membrul său denumire câte o zonă separată în memorie pe care să nu o partajeze cu nimeni altcineva.

Să urmărim în cod implementarea metodelor știind că avem un membru de tip pointer în clasă. Vom prezenta doar implementarile metodelor care conțin atributul denumire, deoarece restul vor rămâne neschimbate.

Conifer::Conifer()
{
	pret = 0.0;
	inaltime = 0.0f;
	denumire = nullptr;
}
 
Conifer::Conifer(const double& pret, const float& inaltime, const char* denumire)
{
	this->pret = pret;
	this->inaltime = inaltime;
 
	if (denumire != nullptr)
	{
		this->denumire = new char[strlen(denumire) + 1];
		strcpy(this->denumire, denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
}
 
char* Conifer::getDenumire() const
{
	return denumire;
}
 
void Conifer::setDenumire(const char* denumire)
{
	if (denumire == nullptr || strlen(denumire) < 3)
	{
		return;
	}
 
	if (this->denumire != nullptr)
	{
		delete[] this->denumire;
	}
 
	this->denumire = new char[strlen(denumire) + 1];
	strcpy(this->denumire, denumire);
}

Se poate observa că atât în constructorul cu toți parametrii cât și în setter-ul pentru atributul denumire am făcut extra validări pentru a preveni atribuirea de denumiri eronate care în realitate nu ar avea sens.

Destructorul

Așa cum îi spune și numele această metodă se ocupă de distrugerea obiectelor atunci când aceastora li se încheie durata de viață în program. La fel ca și constructorii, destructorul are o serie de trăsături ce îl fac ușor de recunoscut într-o clasă după cum urmează:

  1. nu are tip returnat nici măcar void
  2. denumirea destructorului este aceeeași cu a clasei din care face parte, dar pentru a putea fi diferențiat de constructori se pune înaintea lui operatorul ”~“
  3. este unic în clasă
  4. nu are parametri

La fel ca și constructorul default (cel fără parametri), destructorul dacă nu este declarat și implementat, compilatorul va genera el un destructor default.

În continuare vom prezenta cum putem declara destructorul pentru clasa Conifer.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	~Conifer(); // destructorul clasei Conifer
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
};

Dacă o clasă conține cel puțin un membru de tip pointer care este alocat dinamic, varianta de destructor generată de compilator nu va funcționa corect, deoarece nu se va ocupa de dezalocarea memoriei, aceasta fiind datoria noastră. Prin urmare atunci când avem pointeri ca și membri ai clasei pe care i-am alocat dinamic avem datoria să declarăm și să implementăm destructorul pentru a elibera memoria.

După cum putem vedea în exemplul de mai jos destructorul clasei Conifer se va ocupa de eliberarea memoriei pentru atributul denumire.

Conifer::~Conifer()
{
	if (denumire != nullptr)
	{
		delete[] denumire;
	}
}

Destructorul, fie că vorbim de cel generat de compilator sau de cel implementat de programator, nu se apelează explicit de către programator. Acest lucru va fi realizat de către compilator în mod automat atunci când durata de viață a obiectului se încheie.

Constructorul de copiere

Constructorul de copiere (Copy Constructor) este o metodă specială a clasei, deoarece cu ajutorul lui putem copia conținutul unui obiect existent într-unul nou. Acest constructor este unic în clasă și respectă toate caracteristicile unui constructor obișnuit. Dacă nu este declarat și implementat de către programator, compilatorul se va ocupa el de generarea unui copy constructor default.

Constructorul de copiere generat de către compilator face shallow copy, iar dacă în clasă avem cel puțin un membru de tip pointer alocat dinamic, programatorul va trebui să ofere o implementare pentru acest tip de constructor care să facă o copiere profundă (deep copy).

În codul de mai jos vom putea observa modul în care putem declara constructorul de copiere pentru clasa Conifer.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	Conifer(const Conifer& conifer); // constructorul de copiere pentru clasa Conifer
	~Conifer();
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
};

Iar mai jos am implementat acest constructor în așa fel încât să realizeze deep copy.

Conifer::Conifer(const Conifer& conifer)
{
	this->pret = conifer.pret;
	this->inaltime = conifer.inaltime;
 
	if (conifer.denumire != nullptr)
	{
		this->denumire = new char[strlen(conifer.denumire) + 1];
		strcpy(this->denumire, conifer.denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
}

Astfel constructorul de copiere este acum specializat pentru crearea de clone reale pentru obiectele de tip Conifer.

Operatorul de atribuire

Operatorul de atribuire (Operatorul = sau Operatorul de asignare) este folosit pentru a copia conținutul unui obiect existent într-un alt obiect existent. În esență, operatorul de atribuire (asignare) și constructorul de copiere au un comportament similar, deoarece ambele sunt responsabile de copierea valorilor dintr-un obiect în altul. Principala diferență între aceste două funcții membre constă în momentul și contextul în care sunt utilizate.

Constructorul de copiere este folosit atunci când un nou obiect este creat pe baza unui alt obiect existent. Pe de altă parte, operatorul de atribuire este utilizat atunci când un obiect deja existent primește valori din alt obiect, adică obiectul în care se copiază conținutul trebuie să aibă deja alocată memorie și să fi fost inițializat anterior.

La fel ca și în cazul constructorului de copiere, compilatorul va genera el o variantă de operator de asignare dacă programatorul nu îl declară și nu îl implementează. La fel ca în cazul copy constructor-ului, varianta generată de complilator pentru operatorul = face shallow copy, deci va trebui să fie implementat de către programator pentru a face deep copy.

Ceea ce urmează a fi prezentat constituie o variantă de supraîncarcare (overloading) a operatorului de atribuire. Vom discuta mai pe larg ce presupune supraîncărcarea operatorilor în laboratorul următor.

Pentru a putea realiza supraîncărcarea operatorului de asignare trebuie să folosim cuvântul cheie operator urmat de simbolul operatorului (adică ”=“) după cum urmează în codul de mai jos.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	Conifer(const Conifer& conifer);
	Conifer& operator=(const Conifer& conifer); // operatorul de asignare
	~Conifer();
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
};

Iar implementarea acestuia se poate observa mai jos după cum urmează.

Conifer& Conifer::operator=(const Conifer& conifer)
{
	if (this->denumire != nullptr)
	{
		delete[] this->denumire;
	}
 
	this->pret = conifer.pret;
	this->inaltime = conifer.inaltime;
 
	if (conifer.denumire != nullptr)
	{
		this->denumire = new char[strlen(conifer.denumire) + 1];
		strcpy(this->denumire, conifer.denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
 
	return *this;
}

Implementarea anterioară a operatorului de asignare are o vulnerabilitate și anume nu tratează situația în care obiectul este atribuit sieși. Astfel pot apărea probleme cu privire la eliberarea memoriei sau coruperea datelor. Pentru a putea avea o asignare sigură trebuie să facem verificarea de auto-asignare care garantează că operatorul = va funcționa corect în toate situațiile.

Conifer& Conifer::operator=(const Conifer& conifer)
{
	if (this == &conifer) // verificarea de auto-asignare
	{
		return *this;
	}
 
	if (this->denumire != nullptr)
	{
		delete[] this->denumire;
	}
 
	this->pret = conifer.pret;
	this->inaltime = conifer.inaltime;
 
	if (conifer.denumire != nullptr)
	{
		this->denumire = new char[strlen(conifer.denumire) + 1];
		strcpy(this->denumire, conifer.denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
 
	return *this;
}

Este rară situația în care obiectul va fi atribuit sieși, dar este mai bine ca operatorul = să aibă această verificare de auto-asignare pentru a putea evita problemele ce pot apărea la eliberarea și alocarea memoriei în cazul în care clasa conține cel puțin un membru care este pointer.

Cuvântul cheie static

Cuvântul cheie static provine din limbajul C și și-a păstrat funcționalitatea în C++, dar a dobândit și noi utilizări datorită paradigmei Orientate Obiect. În ambele limbaje de programare, static are rolul de a controla durata de viață și vizibilitatea (accesibilitatea) unor variabile sau funcții.

Membri statici

O variabilă statică are un comportament similar cu cel al unei variabile globale, în sensul că trăiește pe toată durata de execuție a programului. În C++, cuvântul cheie static a fost extins pentru a permite declararea membrilor statici într-o clasă. Un membru static aparține clasei în sine, și nu unei instanțe a acesteia, fiind partajat între toate obiectele clasei.

Un atribut static nu poate fi inițializat în cadrul constructorului, deoarece acesta se ocupă de inițializarea membrilor specifici obiectelor, adică membrii non-statici.

Un câmp static în clasă poate fi util de exemplu atunci când ne dorim să numărăm câte instanțe ale clasei respective avem la un anumit moment de timp în program. Pentru a evidenția utilitatea unui membru static vom crea un contor de instanțe pentru clasa Conifer după cum urmează.

#include <iostream>
 
class Conifer
{
	double pret; // membru non-static
	float inaltime; // membru non-static
	char* denumire; // membru non-static
 
	static int numarConifere; // membru static -> in acest exemplu este folosit pe post de contor de instante pentru clasa Conifer
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	Conifer(const Conifer& conifer);
	Conifer& operator=(const Conifer& conifer); // operatorul de asignare
	~Conifer();
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
};

Membrii statici ai unei clase se inițializează întotdeauna în afara clasei, de preferat în fișierul ”.cpp” unde există și implementările celorlalte metode, și obligatoriu nu în constructori.

Inițializarea și prelucrarea unui membru static se poate face după cum urmează.

int Conifer::numarConifere = 0;
 
Conifer::Conifer()
{
	pret = 0.0;
	inaltime = 0.0f;
	denumire = nullptr;
	numarConifere++;
}
 
Conifer::Conifer(const double& pret, const float& inaltime, const char* denumire)
{
	this->pret = pret;
	this->inaltime = inaltime;
 
	if (denumire != nullptr)
	{
		this->denumire = new char[strlen(denumire) + 1];
		strcpy(this->denumire, denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
 
	numarConifere++;
}
 
Conifer::Conifer(const Conifer& conifer)
{
	this->pret = conifer.pret;
	this->inaltime = conifer.inaltime;
 
	if (conifer.denumire != nullptr)
	{
		this->denumire = new char[strlen(conifer.denumire) + 1];
		strcpy(this->denumire, conifer.denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
 
	numarConifere++;
}
 
Conifer::~Conifer()
{
	if (denumire != nullptr)
	{
		delete[] denumire;
	}
 
	numarConifere--;
}

Deși membrul static numarConifere este privat noi ne propunem cumva să avem accest la el. Soluția este să implementăm un getter pentru acesta.

Funcții statice

La fel ca variabia statică funcția statică există pe întreaga durată de viață a programului. În POO o funcție statică la nivel de clasă este foarte asemănătoare cu o metodă. Totuși o metodă primește ca parametru pe prima poziție în lista de parametri pointerul this. În C++ chiar dacă acest parametru nu este vizibil în lista de parametri, la compilare există (acest procedeu ne este ascuns, dar în spate compilatorul adaugă acest parametru la toate funcțiile membre).

O funcție statică la nivel de clasă nu primește pointerul this ca parametru. Îi spunem funcție statică și nu metodă statică din acest motiv.

În exemplul de mai jos am declarat un getter pentru a obține numărul de conifere.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire;
 
	static int numarConifere;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	Conifer(const Conifer& conifer);
	Conifer& operator=(const Conifer& conifer);
	~Conifer();
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
 
	static int getNumarConifere(); // functie statica
};

Iar implementarea o putem observa mai jos.

int Conifer::getNumarConifere()
{
	return numarConifere;
}

Această funcție statică poate fi apelată fie prin numele clasei fie prin intermediul unui obiect după cum urmează în codul sursă de mai jos.

#include "Conifer.h"
 
int main()
{
	std::cout << "Numarul de conifere este: " << Conifer::getNumarConifere() << '\n';
 
	Conifer c1;
 
	std::cout << "Numarul de conifere este: " << Conifer::getNumarConifere() << '\n';
	std::cout << "Numarul de conifere este: " << c1.getNumarConifere() << '\n';
 
	Conifer c2 = c1;
 
	std::cout << "Numarul de conifere este: " << c2.getNumarConifere() << '\n';
 
	return 0;
}

Astfel am putut observa și înțelege cum putem folosi membrii statici și fucțiile statice la nivel de clasă.

Membri constanți

Evident într-o clasă pot exista și membri constanți pentru a permite distincția între obiecte. De obicei un membru constant are un scop bine definit spre exemplu poate fi un cod unic de identificare a obiectului. În lumea reală o persoană este identificată unic după CNP-ul (codul numeric personal) acesteia care știm foarte bine că nu mai poate fi modificat sau atribuit altei persoane.

În cazul exemplului nostru putem să asigurăm unicitatea membrilor constanți folosindu-ne de membrul static. Să urmărim modul în care am declarat în clasa Conifer un atribut constant denumit codUnic.

#include <iostream>
 
class Conifer
{
	double pret;
	float inaltime;
	char* denumire;
 
	const int codUnic; // membru constant
	static int numarConifere;
 
public:
 
	Conifer();
	Conifer(const double& pret, const float& inaltime, const char* denumire);
	Conifer(const Conifer& conifer);
	Conifer& operator=(const Conifer& conifer);
	~Conifer();
 
	double getPret() const;
	float getInaltime() const;
	char* getDenumire() const;
	const int getCodUnic() const; // accesor pentru membrul constant
 
	void setPret(const double& pret);
	void setInaltime(const float& inaltime);
	void setDenumire(const char* denumire);
 
	static int getNumarConifere();
};

Un atribut constant nu poate fi inițializat în interiorul constructorilor, deoarece obiectul deja există și în plus, compilatorul a alocat deja o valoare default pentru membrul constant ce nu mai poate fi modificată. Prin urmare inițializarea membrilor constanți se face obligatoriu în lista de inițializare a constructorului. Această listă de inițializare îi spune compilatorului că înainte de crearea efectivă a obiectului are de atribuit valori pentru anumiți membri.

În exemplul de cod de mai jos am pus în lumină modul în care este inițializat un membru constant.

Conifer::Conifer() : codUnic(++numarConifere)
{
	pret = 0.0;
	inaltime = 0.0f;
	denumire = nullptr;
}
 
Conifer::Conifer(const double& pret, const float& inaltime, const char* denumire) : codUnic(++numarConifere)
{
	this->pret = pret;
	this->inaltime = inaltime;
 
	if (denumire != nullptr)
	{
		this->denumire = new char[strlen(denumire) + 1];
		strcpy(this->denumire, denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
}
 
Conifer::Conifer(const Conifer& conifer) : codUnic(++numarConifere)
{
	this->pret = conifer.pret;
	this->inaltime = conifer.inaltime;
 
	if (conifer.denumire != nullptr)
	{
		this->denumire = new char[strlen(conifer.denumire) + 1];
		strcpy(this->denumire, conifer.denumire);
	}
	else
	{
		this->denumire = nullptr;
	}
}
 
Conifer::~Conifer()
{
	if (denumire != nullptr)
	{
		delete[] denumire;
	}
 
	/* numarConifere--; // nu facem acest lucru deoarece putem avea id-uri duplicate ceea ce ar strica principiul unicitatii*/
}
 
const int Conifer::getCodUnic() const
{
	return codUnic;
}

Implementarea completă a clasei Conifer poate fi descărcată de aici.

Concluzii

În cadrul acestui laborator, am aprofundat conceptul de metode specifice unei clase (constructor de copiere, operator de asignare și destructor) și am clarificat diferențele fundamentale dintre funcții și metode. Funcțiile sunt entități independente, care pot exista în afara unei clase, în timp ce metodele sunt funcții membre asociate direct cu o clasă și care operează asupra instanțelor acesteia.

Un alt aspect important pe care l-am explorat este utilizarea membrilor statici și constanți pentru a implementa mecanisme de generare automată a codurilor unice. Am demonstrat cum membrii statici permit partajarea unei valori comune între toate instanțele unei clase, iar membrii constanți oferă garanția că anumite valori rămân neschimbate pe toată durata de viață a obiectelor. Aceste mecanisme sunt esențiale pentru gestionarea eficientă a resurselor și pentru menținerea consistenței și integrității datelor într-o aplicație OOP.

De asemenea am putut vedea în linii mici ce înseamnă supraîncărcarea unui operator și ce presupune aceasta, mai multe detalii vom da în laboratorul următor când vom studia și alți operatori nu doar pe cel de asignare.

poo-is-ab/laboratoare/03.1728978004.txt.gz · Last modified: 2024/10/15 10:40 by razvan.cristea0106
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0