This is an old revision of the document!
Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
Î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.
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; }
#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
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.
Î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); };
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.