Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
POO - Curs Doc-1.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
868.86 Кб
Скачать

§7. Moştenirea pe mai multe niveluri. Clase virtuale

Atunci când o clasă transmite parametri sau funcţionalităţi altei clase care, la rândul său, se consideră clasă de bază pentru o altă ierarhie de moştenire, vom spune că avem moştenire pe mai multe niveluri.

Cea mai simplă diagramă care exemplifică moştenirea pe mai multe niveluri este următoarea:

Schema de moştenire pe mai multe niveluri se reduce la o aplicare consecutivă a schemei de moştenire pe un singur nivel. Schemele de moştenire pe mai multe niveluri pot fi reprezentate prin grafuri cu mai multe niveluri. Diagramele reprezentate prin grafuri care nu conţin cicluri nu prezintă careva dificultăţi de realizare. Dacă însă grafurile au cicluri, apar unele probleme. Cea mai simplă diagramă de acest fel este de următorul tip:

Pentru o astfel de diagramă există şi interpretări care descriu situaţii reale:

La realizarea unei astfel de scheme, membrii din clasa Persoana vor ajunge în clasa Doctornad pe două căi: una prin clasa Asistent şi cealaltă prin clasa Student. Deci clasa Doctorand va avea două exemplare pentru fiecare membru venit din clasa Persoana. Generalizând cele spuse anterior, s-ar putea afirma că dacă dintr-un vârf ierarhic superior al diagramei există mai multe drumuri către un alt vârf ierarhic inferior, atunci fiecare membru al vârfului ierarhic superior va ajunge în vârful ierarhic inferior de atâtea ori câte drumuri există între aceste vârfuri.

Uneori, în situaţia apariţiei mai multor membri care vin în aceeaşi clasă, apar situaţii de conflict. S-ar putea evita astfel de situaţii, prin utilizarea claselor virtuale la moştenire. Pentru a declara o clasă virtuală la moştenire în schema de moştenire se utilizează cuvântul-cheie virtual înaintea clasei date. Clasele virtuale sunt protejate de dublări ale membrilor.

În continuare, vor fi exemplificate ideile expuse anterior utilizând diagrama precedentă care descrie relaţia dintre clasele A, B, C, D.

#include<iostream.h>

class A

{

protected:

int ia;

public:

A(int i){ia=i;}

};

//-------------

class B:public A

{

protected:

int ib;

public:

B(int ia, int i) : A(ia){ ib=i; }

};

//-------------

class C:public A

{

protected:

int ic;

public:

C(int ia, int i) : A(ia){ ic=i; }

};

//-------------

class D:public B, public C

{

int id;

public:

D(int ia, int ib, int ic, int i) :

B(ia, ib),C(ia, ic){ id=i; }

void afisare()

{

cout<<”ia=”<<ia<<”ib=”<<ib<<”ic=”<<ic

<<”id=”<<id<<endl;

}

};

//==============

void main()

{

D d(7,17,27,37);

d.afisare();

}

Exemplul anterior are o problemă: funcţia membru afisare(), apelată prin intermediul obiectului d, va genera o eroare din motiv că membrul ia nu poate fi afişat, deoarece în clasa D există doi membri cu numele ia. Va trebui schimbat modul de afişare după cum urmează:

void afisare()

{

cout<<”ia=”<<B::ia<<”ib=”<<ib<<”ic=”<<ic

<<”id=”<<id<<endl;

}

afişând membrul ia ce vine prin clasa B. După această schimbare, programul va putea fi lansat, dar clasa D va continua să aibă doi membri cu numele ia. Pentru a scăpa de acest neajuns, clasa A va fi declarată clasă virtuală.

Declarând clasa A clasă virtuală, va fi făcută o schimbare mică, dar esenţială a exemplului. Iată ce se obţine:

#include<iostream.h>

class A

{

protected:

int ia;

public:

A(int i){ia=i;}

};

//-------------

class B:public virtual A

{

protected:

int ib;

public:

B(int ia, int i) : A(ia){ ib=i; }

};

//-------------

class C:virtual public A

{

protected:

int ic;

public:

C(int ia, int i) : A(ia){ ic=i; }

};

//-------------

class D:public B, public C

{

int id;

public:

D(int ia, int ib, int ic, int i) :

B(ia, ib),C(ia, ic){ id=i; }

void afisare()

{

cout<<”ia=”<<ia<<”ib=”<<ib<<”ic=”<<ic

<<”id=”<<id<<endl;

}

};

//==============

void main()

{

D d(7,17,27,37);

d.afisare();

}

Ordinea de execuţie a constructorilor în scheme de moştenire ce conţin clase virtuale este următoarea: sunt evidenţiate clasele virtuale şi sunt executaţi constructorii lor în ordinea în care sunt întâlniţi în descrierea clasei derivate. După ce îşi termină lucrul constructorii virtuali, vor lucra constructorii ne-virtuali ai claselor ne-virtuale, tot în ordinea apariţiei în schema de moştenire.

Membrii transmişi din clasa de bază în clasa derivată au tendinţa de a-şi micşora gradul de acces din punctul de vedere al clasei derivate. În scheme de moştenire pe mai multe niveluri această micşorare poate fi şi mai accentuată, existând posibilitate de existenţă a membrilor din clasa de bază, care ajung inaccesibili într-o oarecare clasă derivată de la anumit nivel. Există posibilitatea de a mări gradul de acces al membrilor transmişi dintr-o clasă de bază într-o clasă derivată. Pentru a mări gradul de acces al unui membru transmis dintr-o clasă de bază într-o clasă derivată, numele membrului precedat de numele clasei şi operatorul rezoluţiei sunt plasate în raza de acţiune a modificatorului de acces necesar.

modificator_acces:

. . .

nume_cl::nume_mem;

Trebuie de ţinut cont că gradul de acces poate fi mărit readucându-l la acelaşi grad pe care la avut în clasa de bază. Nu sunt permise gradaţii de acces intermediare.

De exemplu, fie clasa derivata, care este derivată din clasa baza:

class baza

{

public:

int ib;

};

//--------

class derivata : private baza

{

int id;

public:

baza::ib;

};

Membrul ib în clasa derivata va avea gradul de acces private şi îl readucem la gradul de acces public, pe care îl avea în clasa baza. Nu este posibilă atribuirea gradului de acces intermediar protected.

§8. Specificul utilizării funcţiilor în C++

Caracteristicile impuse funcţiilor de limbajul C sunt, în mare parte, respectate şi în cadrul limbajului C++. Dar există şi o serie de caracteristici noi. Un mecanism important este posibilitatea de supraîncărcare a funcţiilor. Acest mecanism permite o eficienţă sporită la nivel de elaborare şi întreţinere a programelor.

Funcţiile care urmează permit sumarea elementelor unui tablou de tip float şi elementelor unui tablou de tip int.

float suma1(float *tf, int n_e)

{

float s=0;

for (int i=0; i<n_e; i++)

s+=tf[i];

return s;

}

int suma2(int *ti, int n_e)

{

int s=0;

for (int i=0; i<n_e; i++)

s+=ti[i];

return s;

}

Crearea de funcţii cu denumiri diferite, dar care realizează algoritmi similari pentru diferite tipuri ale parametrilor fictivi, poate genera probleme la nivel de apelare, fiindcă trebuie apelată funcţia corectă pentru combinaţia dată de parametri reali. Este necesară atenţie din partea programatorului, fiindcă apelarea funcţiei suma1() - pentru un tablou cu elemente întregi, sau a funcţiei suma2() - pentru un tablou cu elemente reale, va genera rezultate incorecte. O posibilă simplificare pentru astfel de situaţii ar fi crearea de funcţii cu acelaşi nume şi transmiterea responsabilităţii de determinare a variantei corecte a funcţiei apelate în seama compilatorului. Exemplul anterior ar putea fi re-scris astfel:

float suma(float *tf, int n_e)

{

float s=0;

for (int i=0; i<n_e; i++)

s+=tf[i];

return s;

}

int suma(int *ti, int n_e)

{

int s=0;

for (int i=0; i<n_e; i++)

s+=ti[i];

return s;

}

Crearea unor funcţii ce au acelaşi nume va purta denumirea de supraîncărcare a funcţiilor. Pentru a putea fi supraîncărcate, funcţiile trebuie să satisfacă următoarele restricţii:

  • să aibă număr diferit de parametri fictivi;

  • dacă numărul de parametri fictivi este acelaşi, tipul parametrilor fictivi trebuie să difere măcar într-o singură poziţie;

  • tipul returnat de funcţie nu se ia în consideraţie în procesul de supraîncărcare.

Compilatorul de fapt nu lucrează cu numele fizice ale funcţiilor, ci creează nişte denumiri noi, numite signaturi ale funcţiilor. De exemplu, funcţiile

int produs(int i, int j)

{

...

}

şi

float produs(float i, float j)

{

...

}

ar putea avea respectiv următoarele signaturi:

produs_ii

şi

produs_ff

Din câte se vede, signaturile funcţiilor conţin informaţie şi despre tipul parametrilor fictivi, ceea ce este foarte util la determinarea variantei corecte a funcţiei. Deci funcţiile cu acelaşi nume, adică supraîncărcate, au totuşi signaturi diferite. Doar aşa este posibilă o apelare a variantei corecte pentru combinaţia corespunzătoare de parametri reali. Varianta corectă a funcţiei este determinată din confruntarea tipurilor parametrilor fictivi cu tipurile parametrilor reali. În fragmentul ce urmează sunt apelate funcţiile cu nume suma(), definite anterior.

void main()

{

float tf[5]={1.2,4,3,3.4,0};

int ti[3]={1,2,7};

cout<<suma(tf,5)<<" "<<suma(ti,3);

}

Desigur că pentru tabloul tf va fi apelată prima funcţie, iar pentru tabloul ti va fi apelată funcţia a doua.

În limbajele care admit supraîncărcarea funcţiilor, un nume de funcţie poate fi legat cu o mulţime de algoritmi posibili (poate fi supraîncărcată de multe ori). Nu există cerinţe stricte referitor la algoritmii ce ţin de un nume de funcţie şi totuşi este de dorit ca algoritmii realizaţi să fie similari ca idee.

Avantajele supraîncărcării funcţiilor sunt următoarele:

  • simplitatea de elaborare a algoritmilor, având un mecanism mai simplu de interacţiune cu funcţiile;

  • simplitatea de întreţinere a codului elaborat, dat fiind o înţelegere mai simplă a ideilor codificate.

Mecanismul de supraîncărcare a funcţiilor este şi un generator posibil de erori, care de regulă sunt numite ambiguităţi. Cel mai simplu este a descrie posibilele ambiguităţi, prezentând un exemplu. Fie funcţia f1(), definită cum urmează:

void f1(float f)

{

. . .

}

//-----

void f1(double d)

{

. . .

}

În continuare este prezentat un fragment de cod în care sunt efectuate o serie de apelări ale funcţiei date:

int i=7;

float f=1.2;

double d=-3.12;

. . .

f1(f);

f1(d);

f1(2.3);

f1(i);

Prima apelare se referă la prima variantă a funcţiei, având parametrul real de tip float. Următoarele două se referă la cea de-a doua variantă, având parametrul real de tip double. În schimb, ultima apelare va genera o ambiguitate, fiindcă nu e clar care variantă trebuie să fie apelată pentru un parametru real de tip int.

Deoarece constructorul unei clase este în ultimă instanţă o funcţie, lui i se poate aplica mecanismul de supraîncărcare şi deci existenţa claselor care au mai mulţi constructori se datorează anume posibilităţii de supraîncărcare. Drept rezultat o clasă poate avea mai mulţi constructori. De asemenea, o clasă poate avea şi metode supraîncărcate.

Un alt mecanism important este posibilitatea fixării de valori implicite (predefinite) ale parametrilor fictivi ai funcţiei – se utilizează atunci, când setul de valori reale transmise la apelarea funcţiei are o serie de poziţii în care pot fi repetări ale valorilor. De exemplu, funcţia afisareData()

void afisareData(int zi, int luna, int an)

{

cout<<zi <<”.” <<luna <<”.” <<an << endl;

}

poate fi apelată pentru afişarea unor zile de naştere. Pentru studenţii dintr-o grupă există o mare probabilitate că valorile reale din poziţia ce corespunde anului de naştere vor fi egale. Pentru valorile ce ţin de luna naşterii, şansele de egalitate vor fi mai mici, dar tot există. Fixarea valorilor implicite duce la simplificarea expresiei de apelare a funcţiei.

Procesul de fixare a valorilor implicite este ghidat de o serie de reguli:

  • fixarea valorilor implicite pentru parametrii fictivi se face parcurgând lista parametrilor fictivi de la dreapta spre stânga;

  • parametrii fictivi cu valori implicite formează o mulţime compactă, adică nu există parametri fără valori printre parametrii cu valori.

Fixarea valorilor implicite poate fi făcută în două circumstanţe:

  • în antet, la definirea funcţiei;

  • în prototipul funcţiei.

Nu este permisă fixarea valorilor şi în antet, şi în prototip. În schimb dacă în antet nu sunt fixate valori implicite, atunci diferite prototipuri declarate local pot avea diferite seturi de valori implicite. De exemplu, funcţia afisareData()cu valori implicite definite în antet la definirea funcţiei.

void afisareData(int zi,int luna=11,int an=2007)

{

cout <<zi <<”.” <<luna <<”.” <<an << endl;

}

Exemplul ce urmează se referă la fixarea valorilor implicite în prototipul funcţiei. În două funcţii diferite sunt declarate prototipurile funcţiei afisareData(), dar cu seturi diferite de valori implicite.

void afisare2000()

{

void afisareData(int zi, int luna, int an=2000);

. . .

}

şi

void afisare1990()

{

void afisareData(int zi,int luna=1,int an=1990);

. . .

}

La apelarea unei funcţii parametrii reali transmişi în funcţie sunt parcurşi de la stânga spre dreapta, legându-se cu parametrul fictiv corespunzător şi transmiţându-i valoarea sa. Valorile implicite ale parametrilor fictivi sunt substituite de valorile parametrilor reali. Dacă parametri reali sunt mai puţini decât parametri fictivi, atunci parametrii fictivi rămaşi ne-legaţi vor avea ca valori valorile implicite, dacă le au. În caz că nu le au, va fi generată eroare din insuficienţă de parametri reali. De exemplu, dacă va fi apelată funcţia afisareData() din funcţia afisare1990()

afisareData(7, 10, 2004);

afisareData(7, 10);

atunci cea de-a doua apelare va fi echivalentă cu următoarea:

afisareData(7, 10, 1990);

unde în calitate de al treilea parametru real este luată valoarea implicită.

Mecanismul de supraîncărcare a funcţiilor şi fixarea valorilor parametrilor impliciţi pot să ducă la generare de ambiguităţi. În continuare, prin intermediul exemplului care urmează, va fi prezentată o posibilă ambiguitate.

void f(int i1, int i2=0, int i3=0)

{

. . .

}

void f(int i)

{

. . .

}

void main()

{

. . .

f(1, 2, 3); //a)

f(1, 2); //b)

f(1); //c)

. . .

}

Funcţia f() este supraîncărcată, având două variante: cu trei parametri şi cu un parametru. În funcţia main() sunt trei variante de apel al funcţiei f(). Varianta a) va apela funcţia f() cu trei parametri. Varianta b), de asemenea, va apela funcţia f() cu trei parametri, valoarea parametrului al treilea fiind valoarea implicită. Pe când varianta c) - generează ambiguitate fiindcă ar fi bună de apel şi funcţia f() cu trei parametri, având două valori implicite, dar şi cea cu un singur parametru.

Funcţiile membre ale unei clase, de asemenea, pot avea fixate valori implicite pentru parametrii fictivi. Modul de fixare a valorilor implicite ale parametrilor fictivi pentru funcţiile membre nu se deosebeşte de modul descris pentru funcţiile libere.

Fiindcă constructorii unei clase sunt de asemenea funcţii, rezultă că parametrii fictivi ai constructorilor pot de asemenea avea fixate valori implicite. Un constructor cu parametri, dar care are valori implicite pentru toţi parametrii fictivi, este echivalent cu un constructor fără parametri. Acest enunţ poate fi generalizat în felul următor: constructorul care are valori implicite pentru k parametri fictiv este echivalent cu un constructor cu (n-k) parametri, unde n este numărul de parametri fictivi ai constructorului. Fie clasa cl1 având constructor fără parametri

class cl1

{

.

public:

cl1();

.

};

cl1 ob1;

şi clasa cl2 având constructor cu un parametru, dar cu valoare implicită.

class cl2

{

.

public:

cl2(int i=10);

.

};

cl2 ob21(1);

cl2 ob22;

Din cele spuse anterior, clasa cl2 poate crea obiecte, utilizând forma cu parametri şi forma fără parametri a constructorului.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]