Laborator 06 - Moștenire multiplă și agregare

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • definească și să utilizeze namespace-uri
  • recunoască și să înțeleagă conceptul de moștenire multiplă și agregare între mai multe clase
  • construiască ierarhii și legături între clase folosind relațiile de tip “is-a” și “has-a”
  • aplice principiile de reutilizare a codului prin extinderea funcționalităților claselor de bază în clasa derivată, combinând diverse clase și folosind agregarea pentru a crea relații complexe între obiecte

Introducere

Î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.

Moștenirea multiplă

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.

În general, găsirea unei combinații potrivite de clase care să respecte moștenirea multiplă poate fi o provocare, deoarece aceasta poate duce la situații complexe și ambigue. Moștenirea multiplă poate să genereze probleme atunci când două clase părinte au metode sau membri comuni, ceea ce poate crea conflicte sau confuzie în interpretarea acestora în clasa copil. Astfel, devine esențial ca moștenirea multiplă să fie utilizată doar atunci când clasele părinte sunt bine definite, fără suprapuneri de responsabilități sau funcționalități, pentru a evita situațiile de ambiguitate și a păstra codul ușor de întreținut și de extins.

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

Implementarea constructorilor în clasa derivată

Se procedează în manieră similară moștenirii simple cu adăugările de rigoare după cum urmează în exemplele de mai jos.

Implementarea constructorului fără parametri
CameraWeb::CameraWeb() : ProdusComercial(), PiesaElectronica()
{
	rezolutie = 0;
	culoare = nullptr;
}

Se poate observa că elementul de noutate îl constituie apelul a doi constructori, în loc de unul, în lista de inițializare a constructorului fără parametri din clasa CameraWeb.

Implementarea constructorului cu parametri

Î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;
	}
}
Implementarea constructorului de copiere
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;
	}
}

Datorită relației de tip “is-a”, putem trimite un obiect de tip CameraWeb ca parametru în constructorii de copiere ai claselor părinte. Acest proces, cunoscut sub numele de upcasting, permite tratarea unui obiect derivat (de tip CameraWeb) ca fiind de tipul clasei părinte. Upcasting-ul este realizat automat de către compilator și este esențial în moștenire pentru a permite utilizarea obiectelor derivate în locul obiectelor de tipul clasei de bază. Prin acest mecanism, putem să extindem funcționalitățile clasei de bază și să reutilizăm codul, fără a rescrie funcționalitățile în clasa derivată, ceea ce face codul mai flexibil și mai ușor de întreținut.

Implementarea operatorului de asignare în clasa derivată

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

Implementarea destructorului în clasa derivată

CameraWeb::~CameraWeb()
{
	if (culoare != nullptr)
	{
		delete[] culoare;
	}
}

Reamintim faptul că în destructorul clasei derivate nu vom apela explicit destructorii claselor părinte, acest lucru fiind realizat automat de către compilator în momentul în care durata de viață a obiectului de tipul clasei derivate se încheie.

Implementarea operatorului << în clasa derivată

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

Agregare

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.

Agregarea este de două tipuri după cum urmează:

  • Weak (slabă), ceea ce înseamnă că existența obiectului container nu este condiționată de existența atributelor agregate, putem lua spre exemplu un dulap care poate exista și fără să conțină haine.
  • Strong (puternică), ceea ce înseamnă că obiectul container nu poate exista în absența obiectelor agregate, spre exemplu o firmă nu poate exista fără angajați.

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-uri definite de către programator

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.

Concluzii

Î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.

poo-is-ab/laboratoare/06.txt · Last modified: 2025/01/19 22:29 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