Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
În cadrul acestui laborator vom aprofunda conceptele legate de moștenire, extinzând noțiunile discutate în laboratorul anterior și explorând două relații esențiale între clase: relația de tip “is-a” între mai multe clase, cunoscută sub numele de moștenire multiplă și relația de tip “has-a”, cunoscută și sub denumirea de agregare. Vom analiza astfel cum poate o clasă să moștenească simultan trăsături de la mai multe clase și cum pot fi organizate relațiile între clase pentru a modela compoziția unui obiect. Scopul este să înțelegem mai bine felul în care aceste concepte contribuie la crearea unui cod modular, reutilizabil și clar structurat, fiind esențiale în designul eficient al aplicațiilor complexe.
Limbajul de programare C++ permite moștenirea multiplă, ceea ce înseamnă că o clasă derivată poate avea mai multe clase de bază, moștenind astfel trăsături din cel puțin două clase părinte. Acest tip de moștenire oferă flexibilitate și posibilitatea de a combina funcționalități variate din clase distincte într-o singură clasă. Pe lângă C++, și limbajul Python permite moștenirea multiplă, spre deosebire de limbajele Java și C#, care nu acceptă acest tip de moștenire direct pentru clase, în principal din motive legate de ambiguitate și complexitatea managementului de resurse. În aceste limbaje, în schimb, este folosită o abordare bazată pe interfețe pentru a atinge un scop similar.
Moștenirea poate fi vizualizată ca o structură arborescentă, ceea ce explică de ce organizarea claselor într-un astfel de sistem este denumită ierarhie de clase. Într-o ierarhie, fiecare clasă derivată își are rădăcinile în clasele părinte, iar această organizare permite construirea unei structuri logice și ordonate a relațiilor dintre clase. De exemplu, în C++, putem observa în imaginea de mai jos un arbore de fluxuri (ierarhia claselor de fluxuri) în care clasele specifice de intrare și ieșire (precum ifstream
, ofstream
, stringstream
) sunt derivate din clase generale precum istream
, ostream
sau iostream
. Această structură arborescentă oferă atât claritate conceptuală, cât și flexibilitate în reutilizarea și extinderea funcționalităților claselor.
Pentru acest laborator propunem ca și clase părinte ProdusComercial și respectiv PiesaElectronica, iar ca și clasă copil CameraWeb. Dacă am menționat moștenire acest lucru este echivalent cu relația de “is-a” ceea ce înseamnă că orice cameră web este un produs comercial și în același timp orice cameră web este și o piesă electronică.
Declarația clasei ProdusComercial se poate observa în blocul de cod mai jos.
#pragma once #include <iostream> class ProdusComercial { float pret; public: ProdusComercial(const float& pret = 0.0f); ProdusComercial& operator=(const ProdusComercial& produsComercial); friend std::ostream& operator<<(std::ostream& out, const ProdusComercial& produsComercial); };
Iar declarația clasei PiesaElectronica se află în codul sursă de mai jos.
#pragma once #include <string> #include <iostream> class PiesaElectronica { char* brand; public: PiesaElectronica(); PiesaElectronica(const char* brand); PiesaElectronica(const PiesaElectronica& piesaElectronica); PiesaElectronica& operator=(const PiesaElectronica& piesaElectronica); ~PiesaElectronica(); friend std::ostream& operator<<(std::ostream& out, const PiesaElectronica& piesaElectronica); };
Având definite cele două superclase putem să ne ocupăm acum de clasa CameraWeb pentru a putea implementa moștenirea multiplă în C++. La fel ca în cadrul laboratorului precedent trebuie să includem fișierul header aferent fiecărei clase părinte în fișierul header al clasei copil, iar apoi să informăm compilațorul că intenționăm să extindem cele două clase, ProdusComercial și respectiv PiesaElectronica, în clasa derivată CameraWeb.
Prin urmare declarația clasei CameraWeb poate fi observată în codul de mai jos.
#pragma once #include "ProdusComercial.h" #include "PiesaElectronica.h" class CameraWeb : public ProdusComercial, public PiesaElectronica // CameraWeb mosteneste atat clasa ProdusComercial cat si clasa PiesaElectronica { int rezolutie; char* culoare; public: CameraWeb(); CameraWeb(const float& pret, const char* brand, const int& rezolutie, const char* culoare); CameraWeb(const CameraWeb& cameraWeb); CameraWeb& operator=(const CameraWeb& cameraWeb); ~CameraWeb(); friend std::ostream& operator<<(std::ostream& out, const CameraWeb& cameraWeb); };
Se procedează în manieră similară moștenirii simple cu adăugările de rigoare după cum urmează în exemplele de mai jos.
CameraWeb::CameraWeb() : ProdusComercial(), PiesaElectronica() { rezolutie = 0; culoare = nullptr; }
În mod asemănător vom proceda și în cazul constructorului cu toți parametrii din clasa CameraWeb după cum urmează în implementarea de mai jos.
CameraWeb::CameraWeb(const float& pret, const char* brand, const int& rezolutie, const char* culoare) : ProdusComercial(pret), PiesaElectronica(brand) { if (rezolutie <= 0) { this->rezolutie = 0; } else { this->rezolutie = rezolutie; } if (culoare != nullptr) { this->culoare = new char[strlen(culoare) + 1]; strcpy(this->culoare, culoare); } else { this->culoare = nullptr; } }
CameraWeb::CameraWeb(const CameraWeb& cameraWeb) : ProdusComercial(cameraWeb), PiesaElectronica(cameraWeb) { if (cameraWeb.rezolutie <= 0) { rezolutie = 0; } else { rezolutie = cameraWeb.rezolutie; } if (cameraWeb.culoare != nullptr) { culoare = new char[strlen(cameraWeb.culoare) + 1]; strcpy(culoare, cameraWeb.culoare); } else { culoare = nullptr; } }
Operatorul de asignare nefiind un constructor nu are listă de inițializare, prin urmare suntem nevoiți să apelăm explicit operatorii de asignare din superclase înainte de a ne ocupa de zona de memorie a clasei copil, după cum urmează mai jos.
CameraWeb& CameraWeb::operator=(const CameraWeb& cameraWeb) { if (this == &cameraWeb) { return *this; } ProdusComercial::operator=(cameraWeb); PiesaElectronica::operator=(cameraWeb); if (culoare != nullptr) { delete[] culoare; } if (cameraWeb.rezolutie <= 0) { rezolutie = 0; } else { rezolutie = cameraWeb.rezolutie; } if (cameraWeb.culoare != nullptr) { culoare = new char[strlen(cameraWeb.culoare) + 1]; strcpy(culoare, cameraWeb.culoare); } else { culoare = nullptr; } return *this; }
CameraWeb::~CameraWeb() { if (culoare != nullptr) { delete[] culoare; } }
Fiind declarat ca funcție friend în superclase, operatorul << nu este moștenit implicit în clasa derivată. Prin urmare, vom apela explicit operatorul << din fiecare clasă părinte în parte după cum urmează.
std::ostream& operator<<(std::ostream& out, const CameraWeb& cameraWeb) { operator<<(out, (ProdusComercial&)cameraWeb); operator<<(out, (PiesaElectronica&)cameraWeb); out << "Rezolutia camerei web este: " << cameraWeb.rezolutie << " pixeli\n"; out << "Culoarea camerei web este: "; if (cameraWeb.culoare != nullptr) { out << cameraWeb.culoare << '\n'; } else { out << "N/A\n"; } return out; }
Agregarea presupune ca într-o clasă să avem unul sau mai multe atribute de tipul altei clase. Când spunem agregare acest lucru este echivalent cu relația de “has-a” sau “has-many” în funcție de context.
Ca și exemplu didactic vom crea o clasă nouă denumită Laptop care va conține un atribut de tip CameraWeb punând astfel în evidență relația de tip “has-a” dintre două clase. Acestă agregare este de tip weak, deoarece un laptop poate să nu fie dotat cu o cameră web.
#pragma once #include "CameraWeb.h" class Laptop { double pret; CameraWeb cameraWeb; // relatia de has-a public: Laptop(); Laptop(const double& pret, const CameraWeb& cameraWeb); friend std::ostream& operator<<(std::ostream& out, const Laptop& laptop); };
Iar implementările metodelor și a operatorului de afișare pentru clasa Laptop le putem observa în codul de mai jos.
Laptop::Laptop() { pret = 0.0; cameraWeb = CameraWeb(); } Laptop::Laptop(const double& pret, const CameraWeb& cameraWeb) { if (pret <= 0.0) { this->pret = 0.0; } else { this->pret = pret; } this->cameraWeb = cameraWeb; } std::ostream& operator<<(std::ostream& out, const Laptop& laptop) { out << "Pretul laptopului este: " << laptop.pret << " ron\n"; out << "\nDetalii despre camera web a laptopului\n\n"; out << laptop.cameraWeb; return out; }
Codul cu implementările complete ale tuturor claselor prezentate în acest laborator poate fi descărcat de aici.
Namespace-urile (spațiile de nume) în C++ reprezintă un mecanism fundamental care permite organizarea și gestionarea codului sursă. Spațiile de nume ajută la prevenirea coliziunilor de nume și facilitează organizarea codului în module logice.
Pentru a defini un namespace în limbajul C++ se folosește cuvântul cheie namespace urmat de o denumire a acestuia care poate să lipsească. În cazul în care namespace-ul nu are o denumire acesta se numește namespace anonim și poate fi utilizat doar în fișierul în care este declarat.
#pragma once namespace MathHelper { int rest(const int& a, const int& b); int adunare(const int& a, const int& b); int diferenta(const int& a, const int& b); int inmultire(const int& a, const int& b); int impartire(const int& a, const int& b); }
Iar implementările funcțiilor din cadrul namespace-ului se pot observa mai jos.
#include "MathUtils.h" int MathHelper::rest(const int& a, const int& b) { return (b == 0) ? -1 : a % b; } int MathHelper::adunare(const int& a, const int& b) { return a + b; } int MathHelper::diferenta(const int& a, const int& b) { return a - b; } int MathHelper::inmultire(const int& a, const int& b) { return a * b; } int MathHelper::impartire(const int& a, const int& b) { return (b == 0) ? 0 : a / b; }
Testarea acestui namespace este facută în funcția main după cum urmează în codul de mai jos.
#include "MathUtils.h" int main() { int x = 10; int y = 5; std::cout << "Suma numerelor este: " << MathHelper::adunare(x, y) << '\n'; std::cout << "Diferenta numerelor este: " << MathHelper::diferenta(x, y) << '\n'; std::cout << "Produsul numerelor este: " << MathHelper::inmultire(x, y) << '\n'; std::cout << "Rezultatul impartirii numerelor este: " << MathHelper::impartire(x, y) << '\n'; std::cout << "Restul impartirii numerelor este: " << MathHelper::rest(x, y) << '\n'; return 0; }
Unul dintre avantajele majore ale namespace-urilor este faptul că permit definirea unor funcții cu aceeași semnătură, dar cu implementări diferite, fără a crea ambiguitate pentru compilator. Astfel, putem evita conflictele de nume, având libertatea de a implementa funcții similare în namespace-uri diferite. Când apelăm o funcție specificând și namespace-ul, compilatorul va identifica automat funcția corectă, asigurând o structură mai clară și ușor de întreținut, mai ales în proiectele mari sau în situațiile în care folosim cu biblioteci externe.
Închidem prin a menționa faptul că acest laborator ne-a oferit o înțelegere practică a conceptelor de moștenire multiplă și de relație de agregare (“has-a”) între clase, evidențiind astfel modurile prin care putem structura relații logice și eficiente între clase.
Am înțeles cum putem defini clase derivate care extind mai multe clase de bază, oferind posibilitatea reutilizării codului din mai multe surse și creând ierarhii complexe, dar flexibile. Am discutat despre avantajele și riscurile moștenirii multiple, precum posibilele conflicte de nume și gestionarea lor prin tehnici specifice, cum ar fi operatorii din clasele părinte și apelul lor explicit pentru a evita conflictele.
Relația de “has-a” ne-a permis să definim structuri de tip container, unde o clasă conține una sau mai multe instanțe ale altei clase fără a se afla într-o ierarhie de clase. Astfel, am putut organiza eficient datele și responsabilitățile între clase, menținând o separare clară a rolurilor.
Am văzut cum namespace-urile pot ajuta la evitarea conflictelor de nume și ne oferă control asupra identificării funcțiilor și variabilelor specifice din cadrul unui anumit context. Namespace-urile sunt o soluție eficientă pentru organizarea codului, mai ales când avem funcții și clase cu nume identice ca și denumiri în proiecte mari.
Am observat că funcțiile friend nu sunt moștenite în mod implicit în clasele derivate, fiind necesar să le apelăm explicit pentru fiecare clasă părinte atunci când le folosim. Acest lucru ne permite să menținem controlul asupra accesului la datele private între clase, fără a extinde accesul la întreaga ierarhie.
Aceste concepte fundamentale de moștenire multiplă, agregare și gestionare a spațiului de nume ne oferă instrumente puternice pentru a structura și organiza codul eficient. Aceste tehnici vor fi extrem de utile în viitoarele proiecte, facilitând modularizarea codului și scalarea aplicațiilor, reducând în același timp redundanța.