Skip to content

Latest commit

 

History

History
157 lines (145 loc) · 8.6 KB

POO_22_C_2022-05-05.md

File metadata and controls

157 lines (145 loc) · 8.6 KB

Mostenire

O clasa C++ poate mosteni una sau mai multe clase.

class A
{...};

class B: public A
{...};

In momentul in care dorim ca o clasa sa mosteneasca una sau mai multe clase dupa numele ei din declaratie punem : si o lista cu numele claselor mostenite. In fata fiecarui nume de clasa mostenita poate sa apara cuvantul rezervat virtual si poate aparea unul dintre specificatorii public, protected sau private.

class Deriv:[virtual][public/protected/private]Baza1,...
{...};

Un specificator de acces pus in fata clasei mostenite restrange mai mult sau mai putin accesul la membri clasei de baza. Daca nu se pune niciun specificator de acces implicit se considera mostenire private. De regula, mostenirea se face insa public, asa cum este exemplu lui B mosteneste A (de mai sus). Urmatorul tabel ilustreaza vizibilitatea membrilor in functie de specificatorul de acces folosit la derivare.

clasa de baza specificator derivare rezultat
public public public
public protected protected
public private protected
protected ... protected
private ... private
B b;
B* p1 = new B;
A* p2 = new B;
B* p3 = new A;

Primele doua instantieri sunt valabile pentru orice clasa (nu are legatura cu mostenirea). In prima si a doua instantiere se creeaza in memorie doua obiecte. Un obiect instanta B si un obiect instanta A. Obiectul instanta A poarta denumirea de subobiect instantei B. Din obiectul instantei B avem acces la toti membri public si protected din A. In general, cand instantiem o clasa se creeaza cate un subobiect instanta fiecarei clase mostenite direct sau indirect. A treia instantiere este posibila deoarece in urma apelului new B se creeaza obiect instanta B si subobiect instanta A , iar p2 va fi pointer catre subobiect.

delete p1;

La instantierea unei clase dintr-o ierarhie intai se creeaza subobiectele de sus in jos in ierarhie, iar ultimul se creeaza obiectul instanta clasei respective. Distrugerea obiectelor si subobiectelor se face in ordine inversa (de jos in sus) in ierarhie. In instantierea a doua (pentru pointerul p1) se creeaza subobiectul instantei A, apoi obiectul instantei B, iar la delete p1 se sterge mai intai obiectul instantei B, apoi subobiectul instantei A. A patra instantiere nu este posibila deoarece p3 este pointer catre B si nu se creeaza nicio instanta B, atunci p3 nu are pe cine sa retina.

Functii virtuale

class A
{
public:
	virtual void f()
	{
		cout << "f din A" << endl;
	}
};

class B:public A
{
	void f()
	{
		cout << " f din B" << endl;
	}
};

Dupa cum am mai spus, in cadrul unei ierarhii, o functie avand aceiasi semnatura (nume si parametri - ca tip si numar) poate fi imprementata diferit pe nivele diferite. Mecanismul care sta in spate poarta denumirea de polimorfism. Avem posibilitatea ca functia f sa nu o rescriem in clasa B. In aceasta situatie, functia f din A putea fi apelata de un obiect din B, fiind o clasa mostenita; am ales sa suprascriem functia f in B. Un obiect instanta B are posibilitatea de a apela doua functii f: cea din B si cea mostenita din A. Pentru a apela functia din A folosim operatorul de rezolutie.

B b; //static / stack
b.f();
/*
* Apelul b.f() se restrange asupra functiei care este cea
* mai apropiata de obiectul b, adica in cazul nostru, se
* apeleaza f din B. 
*/
B* p1 = new B; //dinamic / heap
p1->f();
/*
* p1->f() se restrange asupra functiei f tot din B
*/
A* p3 = new B;
p3->f();
/*
* Apelul pe->f() se restrange asupra functiei f din A, p3
* fiind pointer catre A. Din cauza ca avem clasa B 
* specializata, extinsa, varianta particularizata sau
* imbunatatita ar fi mai bine ca apelul p3->f() sa se duca
* la functia f din B. Pentru a realiza acest lucru, 
* declaram functia f din A ca fiind virtuala punand 
* cuvantul rezervat virtual in fata functiei. Daca facem
* specificarea virtual in fata functiei f din A, apelul
* p3->f() se duce in clasa B.

O functie poate fi declarata virtuala numai daca este membra unei clase (adica functiile externe nu pot fi virtuale). Intr-o ierarhie, in momentul in care o functie este declarata virtuala, ea va fi virtuala si in clasele derivate. In exemplul nostru, f din B este automat virtuala.

Mecanismul de apelare a functiilor virtuale

Adresele functiilor virtuale la compilare sunt introduse intr-o tabela speciala denumita VMT (virtual memory table). Cand este apelata o functie virtuala este cautata adresa functiei cea mai apropiata in VMT, luandu-se de acolo pointerul catre functia indentificata. Mecanismul de apel al unei functii virtuale in consecinta mai lent decat apelul unei functii membre obisnuite nevirtuale, deoarece inainte de apelul efectiv de functie se face cautare in VMT. Al treilea tip de instantiere devine util in urmatoarea situatie: dorim ca intr-un vector sa punem laolalta atat obiect instanta A cat si obiecte instanta B, deoarece obiectele instantei B sunt si obiecte de tip A. De exemplu, presupuneam ca avem ierarhia:

  • vehicul
    • motocicleta
    • autoturism
    • bicicleta

Presupunem ca avem nevoie sa retinem intr-un vector mai multe vehicule, unele bicicleta, altele motocicleta si alte autoturism. Pentru a putea implementa un astfel de vector avem obligatoriu nevoie sa cream un vector de pointeri catre vehicul.

int n;
vehicul** v;
v = new vehicul*[n];
v[0] = new bicicleta;
v[1] = new motocicleta;
v[2] = ...

In exemplul de mai sus, v[i] fiind pointer catre vehicul el retine adresa subobiectului vehicul pentru fiecare instantiere. Daca metodele din clasa vehicul le punem ca fiind virtuale orice apel de astfel de functie din v[i] se va duce in clasa de jos (bicicleta, motocicleta, autoturism).

cout << v[i].nrRoti() << endl;
// se va duce intr-una dintre clasele derivate daca 
// functia nrRoti este privata in functia vehicul si 
// suprascrisa in clasele derivate.

Constructorii unei clase nu pot fi declarati virtuali, in schimb, destructorul clasei poate fi virtual.

Destructori virtuali

Consideram din nou ierarhia B, B mosteneste A. Scrierea delete p1; distruge ce se afla la adresa p1 adica un obiect instanta B si declanseaza in sus distrugerea subobiectelor, adica, in situatia noastra, distruge subobiectul instanta A. In situatia in care scriem delete p3; se distruge obiectul de la adresa p3 care este, de fapt, subobiect instanta A, iar obiectul instanta B ramane in memorie, rezultand memory-leak. Pentru ca apelul delete p3; sa distruga si obiectul instanta B trebuie sa scriem desctructor in clasa A si sa il punem virtual, in clasa A adaugam virtual~A{}. Apelul delete p3; nu mai este un apel simplu de destructor din A, ci se declanseaza un mecanism de identificare a destructorilor ce trebuie apelati folosind VMT.

Apelul unui constructor dintr-o clasa derivata

Cand instantiem o clasa derivata dintr-o alta clasa, am vazut ca, o data cu obiectul creat se creeaza si cate un subobiect pentru clase mostenite direct sau indirect. Creearea oricarui obiect se face apeland la un constructor. In consecinta, atunci cand se apeleaza un constructor dintr-o clasa derivata, trebuie sa specificam cum se apeleaza cate un constructor pentru fiecare clasa mostenita direct. In exemplul de mai sus: B mosteneste A nescriind niciun constructor in clasele A si B pentru fiecare dintre aceste clase s-a creat automat constructorul implicit fara parametri. Atat pentru obiectul creat, cat si pentru subobiect s-a apelat constructorul implicit corespunzator.

class A
{
public:
	A(){}
	A(int x){cout << x << endl;}
	virtual void f(){...};
	virtual A{}
};

void t(int n){cout << n << endl;}

class B:public A
{
	int m_b;
public:
	B(){}
	B(int x):A(x+1)
	{
		cout << x << endl;
	}
	void f(){...};
}

Pentru primul constructor al clasei B nespecificand apel de constructor din A se cauta implicit in A sa se apeleze constructorul fara parametri. La al doilea constructor din B am aratat ca in A se apeleaza al doilea constructor caruia ii trimite ca parametru valoarea x+1. De regula, parametri de apel ai constructorului din clase mostenita se specifica pentru constructorul din clasa derivata folosind parametri de apel ai constructorului din clasa derivata. In exemplul nostru, constructorul A a fost apelat cu valoarea x+1 unde x este parametru al lui B. Campul m_b se poate initializa ori in corpul constructorului sau in lista de apel de dupa : pusa dupa constructorul B. Tot in aceasta lista de apel pentru constructor pot fi apelate si functii.

B b(7);
// se afiseaza:
8 //A(x-1)
6 //t(x-1)
7 //B(x)