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);
};

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 tip "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 relaț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 din clasa derivată 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ă similară 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ă.

Implementarea operatorului de asignare în clasa derivată

La fel ca în cazul constructorilor va trebui să găsim o modalitate prin care mai întâi ne ocupăm de atributele clasei părinte și pe urmă prelucram datele clasei copil. Operatorul de asignare nefiind un constructor nu are listă de inițializare și va trebui să îl apelăm explicit pe cel din clasa părinte pentru a respecta ordinea pașilor exact la fel ca în cazul constructorilor.

Apartament& Apartament::operator=(const Apartament& apartament)
{
	if (this == &apartament)
	{
		return *this;
	}
 
	this->Locuinta::operator=(apartament); // se apeleaza operatorul de asignare din clasa parinte
	/*(Locuinta&)(*this) = apartament; // este echivalent cu linia de mai sus doar ca este o alta forma de apel*/
 
	numarCamere = apartament.numarCamere;
 
	if (apartament.numeProprietar != nullptr)
	{
		numeProprietar = new char[strlen(apartament.numeProprietar) + 1];
		strcpy(numeProprietar, apartament.numeProprietar);
	}
	else
	{
		numeProprietar = nullptr;
	}
 
	return *this;
}
Implementarea operatorului << în clasa derivată

Vom prezenta în continuare modul de implementare a operatorului de afișare pentru obiectele de tip Apartament respectând în continuare relația de “is-a”.

std::ostream& operator<<(std::ostream& out, const Apartament& apartament)
{
	operator<<(out, (Locuinta&)apartament); // chemam operatorul << din clasa parinte
 
	out << "Numarul de camere din apartament este: " << apartament.numarCamere << " ron\n";
 
	if (apartament.numeProprietar != nullptr)
	{
		out << "Numele proprietarului este: " << apartament.numeProprietar << "\n";
	}
	else
	{
		out << "Numele proprietarului este: N/A\n";
	}
 
	return out;
}

Funcțiile friend dintr-o clasă nu se moștenesc automat de către clasa derivată, motiv pentru care trebuie să apelăm explicit operatorul << definit în clasa de bază. Pentru a înțelege mai bine acest comportament, putem face următoarea analogie: prietenii părinților voștri nu sunt neapărat și prietenii voștri. Relația de prietenie este specifică doar între părinții voștri și acele persoane, iar aceasta nu se extinde automat asupra voastră. La fel, funcțiile friend sunt prietene ale clasei părinte, dar nu devin prietene implicit și pentru clasa derivată.

Acum că am înțeles conceptul de moștenire între două clase, vom putea avansa către implementarea unor ierarhii mai complexe începând cu următorul laborator. Moștenirea ne permite să construim structuri ierarhice, în care clasele pot extinde și reutiliza funcționalități din clasele părinte. Astfel, vom fi capabili să dezvoltăm sisteme mai robuste, eficiente și ușor de întreținut, în care fiecare clasă va adăuga comportamente și atribute specifice, păstrând în același timp funcționalitatea de bază moștenită. Aceste ierarhii de clase vor facilita gestionarea mai bună a codului și îmbunătățirea scalabilității aplicațiilor noastre.

Concluzii

În cadrul acestui laborator, am învățat și aprofundat conceptul de moștenire și am văzut cum poate fi implementată între două clase. Am înțeles că moștenirea este o metodă esențială pentru a reutiliza și extinde codul existent, oferind un cadru flexibil și scalabil pentru dezvoltarea aplicațiilor OOP. Prin utilizarea moștenirii, o clasă derivată poate prelua proprietățile unei clase părinte, oferind astfel posibilitatea de a adăuga sau modifica funcționalități specifice.

Un aspect important pe care l-am discutat este faptul că funcțiile friend nu se moștenesc. Aceste funcții, deși pot accesa membri privați sau protejați ai unei clase, nu sunt automat apelate în clasa derivată. Pentru a înțelege acest comportament, am făcut o analogie simplă: prietenii părinților voștri nu sunt în mod automat și prietenii voștri direcți. Astfel, în cazul în care dorim să accesăm funcționalitățile unei funcții friend dintr-o clasă părinte, va trebui să o apelăm explicit în clasa derivată.

De asemenea, am explorat rolul specificatorului de acces protected, care permite membrilor clasei să fie accesibili în cadrul claselor derivate, dar să rămână inaccesibili din exterior. Această abordare oferă un echilibru între încapsulare și moștenire, protejând datele interne ale clasei părinte, dar permițând totuși clasei copil să le utilizeze.

Un alt concept esențial a fost utilizarea listei de inițializare a constructorului în clasele derivate. În momentul în care instanțiem un obiect din clasa derivată, trebuie să avem grijă să inițializăm corect și membrii clasei părinte. Aceasta se realizează prin apelarea explicită a constructorului părinte în lista de inițializare a constructorului clasei derivate. Am subliniat importanța acestui mecanism, deoarece doar constructorii pot fi apelați în această manieră.

În plus, pentru a accesa metode sau funcții din clasa părinte care nu sunt constructori, trebuie să apelăm explicit funcția dorită folosind sintaxa: numeClasăPărinte::numeMetodă(). Acest apel este necesar pentru a ne asigura că executăm corect comportamentul definit în clasa părinte asupra obiectelor din clasa fiu.

Prin toate aceste concepte și tehnici, am făcut un pas important în utilizarea eficientă a moștenirii în limbajul C++, și suntem pregătiți să explorăm ierarhii mai complexe de clase în laboratoarele viitoare.

poo-is-ab/laboratoare/05.txt · Last modified: 2024/11/15 17:06 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