This is an old revision of the document!


Laborator 05 - Moștenire simplă

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • recunoască și să înțeleagă conceptul de moștenire între două clase
  • construiască legături între clase folosind relația de tip “is-a” (relație de specializare)
  • folosească membrii marcați cu protected și să înțeleagă diferențele dintre accesul public, privat și protejat în moștenire
  • aplice principiile de reutilizare a codului prin extinderea funcționalității clasei de bază în clasa derivată

Introducere

În acest laborator vom extinde lucrul cu clase, introducând conceptul de relații între clase. Vom construi și analiza legături între două clase, folosind relațiile de tip “is-a” și “has-a”, dar vom pune accentul mai mult pe relația de “is-a”. Relația de tip “is-a” reprezintă o formă de moștenire, un principiu fundamental al POO, prin care o clasă derivată (subclasă) preia proprietățile și comportamentele unei clase de bază (superclasă).

Acest tip de relație ne permite să definim ierarhii de clase, să reutilizăm codul și să extindem funcționalitățile claselor, oferind un model de organizare flexibil și scalabil. Moștenirea funcționează similar cu cea din viața reală: clasa derivată preia proprietățile și comportamentele clasei de bază, dar poate adăuga și comportamente noi sau modifica pe cele existente. Astfel, moștenirea facilitează extensibilitatea și întreținerea codului, reducând duplicarea și oferind un mod eficient de a gestiona complexitatea în proiecte de mari dimensiuni.

Moștenirea între două clase

Așa cum am menționat anterior, moștenirea este un principiu fundamental al POO care permite unei clase derivate să preia atât proprietățile (atributele) cât și comportamentele (funcțiile membre) unei clase părinte. Prin acest mecanism, clasa derivată poate să extindă funcționalitatea moștenită prin adăugarea de noi atribute și metode sau prin redefinirea celor existente. Scopul principal al moștenirii este de a promova reutilizarea codului și de a permite o extensie naturală a funcționalităților inițiale, astfel încât să se creeze o structură mai flexibilă și mai ușor de întreținut în cadrul aplicațiilor.

Înainte de a explica moștenirea între două clase vom face o scurtă recapitulare a noțiunilor deja învățate în cadrul laboratoarelor anterioare. Pentru acest laborator propunem clasa Locuinta care are ca și membri pret (de tip float) și adresa (șir de caractere alocat dinamic).

#pragma once
#include <cstring>
#include <iostream>
 
class Locuinta
{
	float pret;
	char* adresa;
 
public:
 
	Locuinta();
	Locuinta(const float& pret, const char* adresa);
	Locuinta(const Locuinta& locuinta);
	Locuinta& operator=(const Locuinta& locuinta);
	~Locuinta();
 
	float getPret() const;
	char* getAdresa() const;
 
	void setPret(const float& pret);
	void setAdresa(const char* adresa);
 
	friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta);
};

Iar implementările pentru funcțiile membre și cea friend le putem observa în codul de mai jos.

#include "Locuinta.h"
 
Locuinta::Locuinta()
{
	pret = 0.0f;
	adresa = nullptr;
}
 
Locuinta::Locuinta(const float& pret, const char* adresa)
{
	this->pret = pret;
 
	if (adresa != nullptr)
	{
		this->adresa = new char[strlen(adresa) + 1];
		strcpy(this->adresa, adresa);
	}
	else
	{
		this->adresa = nullptr;
	}
}
 
Locuinta::Locuinta(const Locuinta& locuinta)
{
	pret = locuinta.pret;
 
	if (locuinta.adresa != nullptr)
	{
		adresa = new char[strlen(locuinta.adresa) + 1];
		strcpy(adresa, locuinta.adresa);
	}
	else
	{
		adresa = nullptr;
	}
}
 
Locuinta& Locuinta::operator=(const Locuinta& locuinta)
{
	if (this == &locuinta)
	{
		return *this;
	}
 
	if (adresa != nullptr)
	{
		delete[] adresa;
	}
 
	pret = locuinta.pret;
 
	if (locuinta.adresa != nullptr)
	{
		adresa = new char[strlen(locuinta.adresa) + 1];
		strcpy(adresa, locuinta.adresa);
	}
	else
	{
		adresa = nullptr;
	}
 
	return *this;
}
 
Locuinta::~Locuinta()
{
	if (adresa != nullptr)
	{
		delete[] adresa;
	}
}
 
float Locuinta::getPret() const
{
	return pret;
}
 
char* Locuinta::getAdresa() const
{
	return adresa;
}
 
void Locuinta::setPret(const float& pret)
{
	if (pret <= 0.0f)
	{
		return;
	}
 
	this->pret = pret;
}
 
void Locuinta::setAdresa(const char* adresa)
{
	if (adresa == nullptr)
	{
		return;
	}
 
	if (this->adresa != nullptr)
	{
		delete[] this->adresa;
	}
 
	this->adresa = new char[strlen(adresa) + 1];
	strcpy(this->adresa, adresa);
}
 
std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta)
{
	out << "Pretul locuintei este: " << locuinta.pret << " ron\n";
 
	if (locuinta.adresa != nullptr)
	{
		out << "Adresa locuintei este: " << locuinta.adresa << "\n\n";
	}
	else
	{
		out << "Adresa locuintei este: inexistenta\n\n";
	}
 
	return out;
}

Pe prima linie a fișierului header în care este definită clasa Locuinta, putem observa utilizarea directivei #pragma once. Aceasta este o instrucțiune specifică compilatorului care indică faptul că fișierul respectiv trebuie inclus și compilat o singură dată, chiar dacă este referit în mod repetat în alte fișiere prin intermediul directivelor #include. Astfel, se previn multiplele incluziuni ale aceluiași fișier header, care ar putea duce la erori de compilare, cum ar fi redefinirea claselor sau funcțiilor. Directiva #pragma once este o alternativă modernă și mai simplă la gardienii clasici ai fișierelor header, adică acele secvențe de cod cu #ifndef, #define și #endif care au același scop.

Dacă voiam să folosim varianta tradițională de scriere a unui fișier header am fi procedat în maniera următoare.

#ifndef LOCUINTA_H
#define LOCUINTA_H
 
#include <cstring>
#include <iostream>
 
class Locuinta
{
    float pret;
    char* adresa;
 
public:
 
    Locuinta();
    Locuinta(const float& pret, const char* adresa);
    Locuinta(const Locuinta& locuinta);
    Locuinta& operator=(const Locuinta& locuinta);
    ~Locuinta();
 
    float getPret() const;
    char* getAdresa() const;
 
    void setPret(const float& pret);
    void setAdresa(const char* adresa);
 
    friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta);
};
 
#endif // LOCUINTA_H

Relația de "is-a" între două clase

Acest tip de relație ne permite să implementăm moștenirea între clase. În acest context, când discutăm despre moștenire, întâlnim următorii termeni esențiali: clasă părinte (denumită și clasă de bază sau superclasă) și clasă derivată (denumită și clasă copil sau subclasă). Clasa părinte reprezintă clasa de la care dorim să preluăm atribute și metode, având posibilitatea să reutilizăm codul existent, în timp ce clasa derivată extinde această funcționalitate, adăugând noi comportamente și caracteristici.

Atunci când dorim să implementăm moștenirea între două clase, este important să respectăm un set clar de reguli. În primul rând, trebuie să stabilim care dintre clase va fi clasa părinte și care va fi clasa copil. Prin procesul de moștenire, afirmăm că un obiect al clasei derivate este, implicit, și un obiect al clasei părinte, datorită relației de tip “is-a” dintre cele două clase. Această relație trebuie să aibă o coerență logică, adică orice instanță a clasei derivate este automat și o instanță a superclasei. De exemplu, omul este un mamifer, iar afirmația că toți oamenii sunt mamifere este corectă. Însă reciproca nu este valabilă, deoarece nu toate mamiferele sunt oameni. Prin urmare, relația de tip “is-a” se aplică doar de la clasa copil către clasa părinte, și nu invers.

În continuare vom căuta o clasă care poate să extindă și să respecte relația de “is-a” cu clasa Locuinta. Vom propune clasa Apartament care respectă regulile descrise mai sus, deoarece orice apartament este o locuință. Clasa Apartament are ca și atribute numarCamere (de tip întreg) și numeProprietar (șir de caractere alocat dinamic).

În limbajul C++ pentru ca o clasă să moștenească o altă clasă se folosește următoarea sintaxă: class NumeClasaDerivata : specificator de acces pentru moștenire NumeClasaParinte și apoi corpul clasei derivate.

Să urmărim în cod cum putem face clasa Apartament să moștenească clasa Locuință.

#pragma once
#include "Locuinta.h"
 
class Apartament : public Locuinta // clasa Apartament mosteneste clasa Locuinta
{
	int numarCamere;
	char* numeProprietar;
 
public:
 
	Apartament();
	Apartament(const float& pret, const char* adresa, const int& numarCamere, const char* numeProprietar);
	Apartament(const Apartament& apartament);
	Apartament& operator=(const Apartament& apartament);
	~Apartament();
 
	int getNumarCamere() const;
	char* getNumeProprietar() const;
 
	void setNumarCamere(const int& numarCamere);
	void setNumarCamere(const char* numeProprietar);
 
	friend std::ostream& operator<<(std::ostream& out, const Apartament& apartament);
};

Deși clasa Apartament moștenește clasa Locuinta din cauza faptului că membrii clasei Locuinta sunt marcați din default cu private nu vom avea acces la ei în clasa derivată.

Specificatorul de acces protected

Pentru a putea face câmpurile clasei Locuinta să fie vizibile în clasa copil, dar să nu poată fi în continuare accesate din exterior de oricine le vom marca cu protected după cum urmează în codul de mai jos.

#pragma once
#include <cstring>
#include <iostream>
 
class Locuinta
{
protected: // specificatorul de acces protected
 
	float pret;
	char* adresa;
 
public:
 
	Locuinta();
	Locuinta(const float& pret, const char* adresa);
	Locuinta(const Locuinta& locuinta);
	Locuinta& operator=(const Locuinta& locuinta);
	~Locuinta();
 
	float getPret() const;
	char* getAdresa() const;
 
	void setPret(const float& pret);
	void setAdresa(const char* adresa);
 
	friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta);
};

Astfel acum câmpurile pret și adresa vor fi vizibile și în clasa derivată, dar vor rămâne inaccesibile în funcția main sau în orice altă clasă care nu moștenește clasa Locuinta.

Implementarea metodelor și a funcțiilor friend în clasa derivată

În continuare vom prezenta modul în care trebuiesc implementate toate funcționalitățile clasei Apartament astfel încât realția de “is-a” să fie satisfăcută și să reutilizăm codul din clasa Locuinta.

Implementarea constructorilor clasei derivate

Atunci când dorim să construim un obiect de tipul clasei derivate, trebuie să ținem cont de faptul că, mai întâi, trebuie inițializate și gestionate toate datele și resursele moștenite din clasa părinte. Constructorul clasei derivate va apela constructorul clasei părinte pentru a asigura corectitudinea inițializării componentelor moștenite. Astfel, acest proces garantează că toate proprietățile părintelui sunt gestionate corespunzător înainte de a se trece la inițializarea specifică clasei derivate.

Să urmărim cu atenție mai jos implemetarea constructorului fără parametri pentru clasa Apartament.

Apartament::Apartament() : Locuinta()
{
	numarCamere = 0;
	numeProprietar = nullptr;
}

Se poate observa că, înainte de a inițializa câmpurile specifice clasei Apartament, am apelat constructorul fără parametri al clasei Locuință în lista de inițializare a constructorului clasei derivate. Astfel, am reutilizat codul din clasa părinte printr-un simplu apel, asigurând inițializarea corectă a membrilor moșteniți. Este important de menționat că această listă de inițializare poate fi utilizată exclusiv în cazul constructorilor, fiind o metodă eficientă de a gestiona inițializarea clasei părinte.

În continuare vom implementa constructorul cu parametri pentru clasa copil urmând același principiu ca la constructorul fără parametri.

Apartament::Apartament(const float& pret, const char* adresa, const int& numarCamere, const char* numeProprietar) : Locuinta(pret, adresa)
{
	this->numarCamere = numarCamere;
 
	if (numeProprietar != nullptr)
	{
		this->numeProprietar = new char[strlen(numeProprietar) + 1];
		strcpy(this->numeProprietar, numeProprietar);
	}
	else
	{
		this->numeProprietar = nullptr;
	}
}

Se poate observa din implementarea anterioară că am deschis lista de inițializare pentru acest constructor unde am chemat constructorul cu parametri al clasei părinte (clasa Locuinta).

Constructorul cu parametri al clasei derivate include în lista sa de argumente și parametrii necesari pentru a apela constructorul corespunzător din clasa părinte. Acești parametri sunt transmiși în lista de inițializare a constructorului clasei copil atunci când este apelat constructorul din superclasă, facilitând astfel inițializarea corectă a membrilor moșteniți din clasa părinte. Acest mecanism permite transmiterea valorilor necesare direct către clasa părinte, asigurând o organizare clară și o reutilizare eficientă a codului.

În manieră simlară se implementează și constructorul de copiere al clasei derivate.

Apartament::Apartament(const Apartament& apartament) : Locuinta(apartament)
{
	numarCamere = apartament.numarCamere;
 
	if (apartament.numeProprietar != nullptr)
	{
		numeProprietar = new char[strlen(apartament.numeProprietar) + 1];
		strcpy(numeProprietar, apartament.numeProprietar);
	}
	else
	{
		numeProprietar = nullptr;
	}
}

Astfel, am evidențiat reutilizarea codului prin faptul că, în momentul în care construim un obiect de tipul clasei derivate, nu este necesar să redefinim sau să duplicăm funcționalitățile și datele din clasa părinte. Prin simplul apel al constructorului clasei părinte în lista de inițializare a constructorului clasei derivate, putem păstra claritatea și concizia codului sursă. Aceasta reprezintă un avantaj major al moștenirii în POO, permițând extinderea funcționalității fără a compromite principiile de reutilizare și organizare.

Implementarea destructorului în clasa derivată

Destructorul clasei derivate se implementează la fel ca un destructor obișnuit, adică vom elibera memoria alocată dinamic pentru membrii care sunt de tip pointer.

Apartament::~Apartament()
{
	if (numeProprietar != nullptr)
	{
		delete[] numeProprietar;
	}
}

În destructorul clasei derivate nu apelăm destructorul clasei părinte. Acest lucru va fi realizat automat de către compilator în mod corect fără a fi nevoie de intervenția noastră.

poo-is-ab/laboratoare/05.1728826944.txt.gz · Last modified: 2024/10/13 16:42 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