8.2. Necesitatea func?iilor virtuale

8.1. No?iune de polimorfism

8.1. No?iune de polimorfism

No?iunea de polimorfism este exprimat? prin fraza „O interfa??, metode multiple”. D?m c?teva exemple de polimorfism obi?nuit:

  • opera?ii aritmetice. Scriem i+k, unde variabilele i ?i k sunt, de exemplu, de tipul int, ?i tot a?a scriem x+y, unde variabilele x ?i y sunt, de exemplu, de tipul double;
  • apelul func?iilor. Scriem max(i, j, k) pentru parametrii de tipul, de exemplu, unsigned short, ?i la fel scriem max(a, b, c) pentru parametrii de tipul, de exemplu, float.

Suntem interesa?i de no?iunea de polimorfism ca de una dintre cele mai importante caracteristici ale POO.

Polimorfismul ?n POO este proprietatea de a defini una ?i aceea?i interfa??, at?t ?n clas? de baz?, c?t ?i ?n cele derivate, la o ac?iune oarecare, care totu?i se execut? ?n mod specific pentru fiecare clas? aparte.

Polimorfismul ?n C++ are dou? aspecte: polimorfismul ?n timpul compil?rii (engl. static polymorphism) ?i polimorfismul ?n timpul rul?rii (engl. run-time polymorphism). Polimorfismul ?n timpul compil?rii se ob?ine prin supra?nc?rcarea func?iilor ?i operatorilor ?mpreun? sau f?r? proprietate de mo?tenire. Polimorfismul ?n timpul rul?rii (numit, mai rar, polimorfism dinamic) este realizat prin mo?tenire ?i func?ii virtuale. Deoarece despre supra?nc?rcarea func?iilor ?i operatorilor am discutat ?n deajuns ?n capitolele precedente, trecem la cercetarea func?iilor virtuale.
_________________________
Autorul: dr.conf. S. Pereteatcu

 

8.2. Necesitatea func?iilor virtuale

8.2. Necesitatea func?iilor virtuale

?nainte de a discuta no?iunea de func?ie membr? virtual? a clasei, vom ?ncerca s? demonstr?m prin exemple necesitatea func?iilor virtuale.

Presupunem c? o problem? oarecare cere prelucrarea vectorilor (sau listelor) cu elementele ce reprezint? diferite numere ra?ionale, cum ar fi frac?ii ra?ionale pozitive, ra?ionale f?r? semn, ra?ionale arbitrare.

Cre?m clasa vector pentru acumularea numerelor ra?ionale. Ca demonstrarea noastr? s? fie mai conving?toare, presupunem c? clasa vector se creeaz? imediat dup? crearea clasei fractie_rationala, adic? atunci c?nd clasele rational_fara_semn ?i rational nu erau cunoscute. De aceea, ad?ug?m la sf?r?itul fi?ierului ”fr.cpp” urm?torul cod de declarare a clasei vector:

class vector
{
 protected:
  fractie_rationala *pel; // Pointer la vector de elemente
  int n; // num?rul de elemente ?n vector
 
 public:
  vector(int ninit) // num?rul de elemente se indic? la
                    // crearea vectorului
  {
     n = ninit;
     pel = new fractie_rationala[n]; // alocarea vectorului
  }
 
  ~vector()
  {
     delete []pel; // destructorul elibereaz? memoria
                   // ocupat? de vector
  }
 
  fractie_rationala &operator[](int i) // accesul la elemente
  {
     return *(pel+i);
  }
 
  ostream &print(ostream &stream = cout) // afi?area vectorului
  {
     for(int i=0; i<n; i++) (pel+i)->print(stream) << endl;
     return stream;
  }
 
  void sort() // sorteaz? elementele vectorului ?n ordinea
  {           // cresc?toare
     for(int i=1; i<n; i++)
       for(int j=i; (j>0) && (*(pel+j)<*(pel+j-1)); j--)
       {  // aici schimb?m cu locurile dou? elemente
          fractie_rationala t = *(pel+j);
          *(pel+j) = *(pel+j-1);
          *(pel+j-1) = t;
       }
  }
};

Clasa vector are dou? c?mpuri. C?mpul pel este un pointer la primul element al vectorului. Memoria pentru vector se aloc? dinamic la crearea obiectului, totodat? ob?ine valoare ?i pointerul pel. ?n c?mpul n se va p?stra num?rul de elemente ale vectorului. Num?rul de elemente se indic? ca argumentul constructorului la crearea obiectului.

La eliminarea obiectului vector ar fi de dorit s? eliber?m memoria alocat? pentru elementele acestui vector, ceea ce ?i efectueaz? destructorul definit ?n clasa vector.

Clasa vector asigur? accesul la elementele vectorului prin supra?nc?rcarea operatorului binar de indexare [ ]. Re?ine?i c? operatorul [ ] ?ntoarce ca rezultat o referin?? la elementul cu indicele dat i. Aceasta ne d? posibilitatea de a accesa elementele vectorului at?t pentru aflarea valorilor lor, c?t ?i pentru atribuirea valorilor noi.

De exemplu, dac? v este o instan?? a clasei vector, iar f1 ?i f2 sunt dou? frac?ii ra?ionale, atunci f1=v[0] atribuie instan?ei f1 valoarea primului element al vectorului, iar v[1]=f2 atribuie elementului cu indicele 1 al vectorului valoarea f2.

Func?ia membr? print() afi?eaz? elementele vectorului unul dup? altul ?n coloan?. Pentru afi?area elementelor, ?n bucla for se folose?te func?ia print() din clasa fractie_rationala.

Ultima func?ie membr? sort() sorteaz? prin inser?ie simpl? elementele vectorului ?n ordinea cresc?toare. Trebuie de subliniat c? compararea elementelor este posibil? datorit? operatorului de conversie (double). Memoriza?i cum se acceseaz? elementele vectorului ?n func?ia membr? a clasei vector. Mai sunt ?i alte posibilit??i.

?n continuare mai sunt prezentate dou? versiuni ale func?iei sort(). Una dintre ele folose?te pointerul ?ncorporat this ?i operatorul [ ] supra?nc?rcat ?n clasa vector. Alta folose?te operatorul standard [ ] pentru tipul fractie_rationala. Ve?i ?ncerca pe r?nd toate trei func?ii sort() ?i v? ve?i convinge c? rezultatele vor fi identice.

Pute?i introduce ?n declararea clasei vector ?n fi?ierul ”fr.cpp” toate cele trei versiuni ale func?iei sort(), ?ns? de fiecare dat? c?nd lansa?i programul dou? dintre ele trebuie s? fie comentate cu /* ?i */.

Versiunea func?iei sort() cu pointerul this arat? astfel:

void sort()
   {
     for(int i=1; i<n; i++)             
       for(int j=i; (j>0) && ((*this)[j]<(*this)[j-1]); j--)
         {  // aici schimb?m cu locurile dou? elemente
            fractie_rationala t = (*this)[j];
            (*this)[j] = (*this)[j-1];
            (*this)[j-1] = t;
         }
   }
//Versiunea func?iei sort() cu operatorul standard [] arat? astfel:
void sort()
   {
     for(int i=1; i<n; i++)             
       for(int j=i; (j>0) && (pel[j]<pel[j-1]); j--)
       {  // aici schimb?m cu locurile dou? elemente
          fractie_rationala t = pel[j];
          pel[j] = pel[j-1];
          pel[j-1] = t;
       }
    }

Aducem un exemplu care demonstreaz? cum lucreaz? clasa vector. Revenim la fi?ierul ”rfs.cpp” ?i ad?ug?m ?n el la func?ia main() urm?torul cod:

vector vr(6);
   vr[0]=fractie_rationala(2, 3);
   vr[1]=rational_fara_semn(1, 3, 5);
   vr[2]=rational(-2, 1, 3);
   vr[3]=rational_fara_semn(1, 1, 4);
   vr[4]=fractie_rationala(4, 5);
   vr[5]=rational(-1, 2, 3);
 
   cout << "Vectorul vr initial:" << endl;
   vr.print();
 
   vr.sort();
   cout << "Vectorul vr dupa sortarea:" << endl;
   vr.print();

Codul ad?ugat nu este dificil a fi ?n?eles. Am creat vectorul vr din 6 elemente – obiecte fractie_rationala. Aici trebuie s? ?n?elegem c? ini?ial toate elementele, fiind trecute prin constructorul fractie_rationala cu argumentele implicite, vor avea valoarea 0/1. Apoi, fiec?rui element aparte i se atribuie o valoare personal?.

?n unele cazuri, va fi aplicat? regula de atribuire de la clasa derivat? la clasa de baz?. ?n cazul c?nd se atribuie de la clasa rational_fara_semn, partea ?ntreag? se pierde. ?n cazul c?nd se atribuie de la clasa rational, se pierde ?i semnul. Se afi?eaz? vectorul dup? atribuiri, apoi s? sorteaz? ?i se afi?eaz? vectorul sortat. Rezultatul acestui fragment va fi urm?torul:

Vectorul vr initial:
2/3
3/5
1/3
1/4
4/5
2/3

Vectorul vr dupa sortarea:
1/4
1/3
3/5
2/3
2/3
4/5

?ntr-adev?r, vectorul vr este compus din obiecte fractie_rationala ?i nu are nimic comun cu clasele rational_fara_semn ?i rational. S-a ob?inut c? obiectele claselor derivate s-au pierdut dup? atribuire. Aici nu putem face nimic. Totul va r?m?ne a?a cum este.

Pentru a atinge scopul nostru – de a lucra simultan cu diferite clase, s? alegem alt? cale. Vom declara o alt? clas? de vectori, pe care o vom numi vectorp. Fiecare element al acestor vectori va fi nu un obiect fractie_rationala, ci un pointer spre obiect al clasei fractie_rationala. Noua clas? iar??i va fi ad?ugat? la sf?r?itul fi?ierului ”fr.cpp”. Fi?i aten?i, s? nu confunda?i fi?ierele.

class vectorp
{
 protected:
   fractie_rationala **pel; //Aten?ie! Pointer spre vectorul de pointeri
   int n; // num?rul de pointeri ?n vector
 
 public:
    vectorp(int ninit)
    {
       n = ninit;
       pel = new fractie_rationala* [n]; // aloc?m memoria pentru
                                         // vector de n pointeri
    }
 
    ~vectorp()
    {
      for(int i=0; i<n; i++) // mai ?nt?i elimin?m obiectele indicate de
         delete pel[i];      // c?tre pointeri din vectorul de pointeri
      delete []pel;          // apoi elimin?m ?nsu?i vectorul de pointeri
    }
 
    fractie_rationala* &operator[](int i)
    {
       return *(pel+i); // acum returneaz? referin?a la un
                        // pointer din vector
    }
 
    ostream &print(ostream &stream = cout)
    { // vom afi?a obiectele indicate de c?tre pointeri din vector
       for(int i=0; i<n; i++)                
          (*(pel+i))->print(stream) << endl;
       return stream;
    }
 
    void sort()
    {
       for(int i=1; i<n; i++) 
         for(int j=i; (j>0) &&(**(pel+j)<**(pel+j-1)); j--)
          {  // aici schimb?m cu locurile valorile la doi
             // pointeri din vector
             fractie_rationala *pt = *(pel+j);
             *(pel+j) = *(pel+j-1);
             *(pel+j-1) = pt;
          }
    }
};

Declara?ia clasei vectorp este comentat? destul de detaliat. Ad?ug?m doar c?teva observa?ii. ?n aceast? clas? c?mpul pel reprezint? pointer la un vector, fiecare element al c?ruia este un pointer spre o instan?? a clasei fractie_rationala. Vom considera c? instan?ele la care vor indica pointerii din vector vor fi alocate dinamic ?i ele, ?i, ?n cazul lichid?rii obiectului vectorp, trebuie s? fie eliminate.

De aceea, ?n destructorul clasei vectorp, ?n compara?ie cu destructorul clasei vector, a ap?rut o bucl? for ?n care se elibereaz? memoriile ocupate de c?tre instan?ele indicate de c?tre pointerii din vector. Acum operatorul de indexare [ ] ?ntoarce referin?e la pointeri - elementele vectorului, pe c?nd func?ia print() afi?eaz? instan?ele indicate de c?tre ace?ti pointeri.

Algoritmul func?iei sort() a r?mas acela?i, numai c? ?n cazul schimbului se modific? adresele ce se con?in ?n pointeri. Instan?ele indicate de c?tre pointeri r?m?n pe loc. Dup? sortare, primul pointer din vector va indica spre instan?a cu cea mai mic? valoare, iar ultimul pointer din vector va indica instan?a cu cea mai mare valoare.

Pentru verificarea clasei vectorp, ad?ug?m la func?ia main() ?n fi?ierul ”rfs.cpp” urm?torul cod:

   vectorp vrp(6);
   vrp[0] = new fractie_rationala(2, 3);
   vrp[1] = new rational_fara_semn(1, 3, 5);
   vrp[2] = new rational(-2, 1, 3);
   vrp[3] = new rational_fara_semn(1, 1, 4);
   vrp[4] = new fractie_rationala(4, 5);
   vrp[5] = new rational(-1, 2, 3);
 
   cout << "Vectorul vrp initial:" << endl;
   vrp.print();
 
   vrp.sort();
   cout << "Vectorul vrp dupa sortarea:" << endl;
   vrp.print();

Aici momentul cel mai important este crearea dinamic? cu operatorul new a obiectelor de diferite clase. ?n toate cazurile va lucra operatorul new supra?nc?rcat ?n clasa fractie_rationala, fiindc? el nu a mai fost suprascris ?n clasele rational_fara_semn ?i rational. Aminti?i-v? c? acest operator, pe l?ng? repartizarea memoriei, mai afi?eaz? ?i un mesaj de control.

Dar nu aceasta este important. La urma urmei, pute?i ?terge acest mesaj sau chiar ?i supra?nc?rcarea operatorului new din clasa fractie_rationala (?n ultimul caz va trebui s? ?terge?i ?i supra?nc?rcarea operatorului delete din clasa fractie_rationala). Important este faptul c? diferite obiecte create cu new nu s-au pierdut ?i adresele lor s-au memorizat ?n vectorul de pointeri vrp. Totu?i, rezultatul afi?at de c?tre ultimul fragment v? va descuraja la prima vedere:

Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Vectorul vrp initial:
2/3
3/5
1/3
1/4
4/5
2/3

Vectorul vrp dupa sortarea:
1/4
1/3
3/5
2/3
2/3
4/5
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!

El nu difer? deloc de cel precedent. Afi??rile de control de tipul ”Sunt in …” nu le lu?m ?n considera?ie.

De ce totu?i s-a primit a?a? Foarte simplu, ?n func?ia print(), membr? a clasei vectorp, ?n bucla for se apeleaz? func?ia print(), membr? a clasei fractie_rationala. Aceast? din urm? func?ie print() „nu ?n?elege” c? pointerul vrp[1] indic? spre obiectul cu valoarea 1,3/5, ea „nici nu are grij? de aceasta”, fiindc? a fost creat? c?nd clasele derivate nici nu existau.

Ea „consider?” c? are un obiect fractie_rationala ?i afi?eaz? 3/5, la fel ?i pentru celelalte obiecte derivate. Nu putem „reeduca” func?ia print() membr? a clasei fractie_rationala, ea cum „nu a ?tiut” nimic despre clasele derivate la momentul c?nd a fost creat?, a?a ?i nu va ?ti despre ele. ?ns?, putem proceda ca ?n cazul ?n care pointerul vrp[i] indic? spre o instan?? fractie_rationala s? fie apelat? func?ia print() din aceast? clas?.

?n cazul c?nd pointerul vrp[i] indic? spre o instan?? rational_fara_semn s? fie apelat? func?ia print() din aceast? clas?, iar ?n cazul c?nd pointerul vrp[i] indic? spre o instan?? rational s? fie apelat? func?ia print() membr? a clasei rational. Aceasta se face cu ajutorul func?iilor membre virtuale.
_________________________
Autorul: dr.conf. S. Pereteatcu

 

8.3. No?iune de func?ie virtual?

8.3. No?iune de func?ie virtual?

O func?ie membr? devine func?ie virtual?, dac? vom scrie cuv?ntul-cheie virtual ?n antetul func?iei la declararea ei ?n clas?. C?nd o func?ie virtual? se aplic? direct la o instan?? a clasei, apelul acestei func?ii nu se deosebe?te de apelul oric?rei altei func?ii care nu este virtual?. Este clar la ce obiect se aplic?, ?i anume: reprezentantul c?rei clase este acest obiect.

Un a?a apel se rezolv? ?n timpul compil?rii ?i edit?rii de leg?turi, a?a c? la momentul rul?rii programului apelul acesta este deja rezolvat. Apeluri de acest tip se numesc leg?turi ini?iale, timpurii sau statice (engl. early binding).

Altfel stau lucrurile, dac? o func?ie virtual? se apeleaz? printr-un pointer sau o referin??. ?n acest caz poate s? nu fie clar la ce obiect indic? pointerul, sau ce obiect este trimis prin referin?? – cel de baz? sau unul dintre cele derivate (doar ?ti?i regulile de atribuire ?ntre clasele de baz? ?i cele derivate). ?n a?a condi?ii, tipul obiectului poate fi aflat numai ?n timpul rul?rii, c?nd programul a ajuns la instruc?iunea ?n care se con?ine acest apel al func?iei noastre.

Dac? func?ia noastr? este virtual?, rezolvarea apelului ei va fi am?nat? p?n? ?n momentul ?n care programul rulat nu ajunge la acest apel. Adic?, func?ia va fi dinamic apelat? ?n dependen?? de tipul obiectului spre care indic? pointerul, sau care a fost trimis prin referin??. Astfel de apeluri se numesc leg?turi ulterioare, t?rzii sau dinamice (engl. late binding).

Deocamdat? nu vom intra ?n detaliile de realizare a mecanismului leg?turilor ulterioare (el poate fi realizat, de exemplu, prin Tabelele Metodelor Virtuale, engl. VMTVirtual Methods Table). D?m forma general? de declarare a func?iilor virtuale ?i vom modifica exemplul nostru pentru a demonstra utilizarea acestor func?ii.

Func?ia virtual? este o func?ie membr? a clasei, care, fiind apelat? prin pointer sau prin referin?? la un obiect al acestei clase, se apeleaz? dinamic.

Adic?, apelul dat va fi rezolvat nu ?n timpul compil?rii ?i edit?rii de leg?turi, ci la momentul apelului func?iei pe parcursul rul?rii programului.

Forma general? de declarare a unei func?ii virtuale este urm?toarea:

virtual [tip ] [nume_clas?::]nume_func?ie

                                  ([list?_parametri])

{

   …

}

sau

tip virtual [nume_clas?::]nume_func?ie

                                  ([list?_parametri])

{

   …

}

Dac? o func?ie virtual? se suprascrie ?ntr-o clas? derivat?, la suprascriere cuv?ntul-cheie virtual poate fi omis.

Func?ia virtual? nu poate fi declarat? ca o func?ie generic?.

Func?ia declarat? ?ntr-o clas? ca func?ie membr? virtual?, fiind mo?tenit?, r?m?ne virtual? ?i ?n orice alt? clas? derivat? a acestei clase de baz?. ?n orice clas? derivat? func?ia virtual? poate fi, la necesitate, suprascris?.

Revenim la exemplul nostru. ?n primul r?nd, vom transforma func?ia print() ?n una virtual?. Pentru aceasta, ?n fi?ierul ”fr.cpp” scriem cuv?ntul-cheie virtual ?nainte de antetul func?iei print() ?n declararea clasei fractie_rationala, astfel ca antetul nou s? fie:

virtual ostream &print(ostream &stream)

F?r? a face alte modific?ri, trecem la fi?ierul ”rfs.cpp”. ?l compil?m ?i lans?m programul. Efectul ultimei modific?ri se observ? imediat:

Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!

Vectorul vrp initial:
2/3
1,3/5
-2,1/3
1,1/4
4/5
-1,2/3

Vectorul vrp dupa sortarea:
1,1/4
-2,1/3
1,3/5
2/3
-1,2/3
4/5
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!

Observ?m c? obiectele s-au afi?at, ?i anume acelea care au fost create ?i alocate dinamic. Totu?i, sortarea nu s-a f?cut corect. Este clar de ce. Din cauza compar?rii, mai bine spus, din cauza conversiei. Operatorul de conversie la double fiind non-virtual a fost folosit permanent din clasa de baz? fractie_rationala. S?-l modific?m ?n unul virtual. Schimb?m antetul lui ?n fi?ierul ”fr.cpp” ?n unul:

virtual operator double()

Iar??i compil?m ?i lans?m programul ”rfs.cpp”, rezultatul multa?teptat va fi:

Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!
Sunt in new pentru fr!

Vectorul vrp initial:
2/3
1,3/5
-2,1/3
1,1/4
4/5
-1,2/3

Vectorul vrp dupa sortarea:
-2,1/3
-1,2/3
2/3
4/5
1,1/4
1,3/5

Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru fr!

Acum ?i afi??rile, ?i sortarea sunt corecte. Atrage?i aten?ia c? fragmentul precedent din func?ia main(), care utilizeaz? clasa vector, afi?eaz? acela?i rezultat pe care el l-a afi?at ?i f?r? func?ii virtuale.

Exerci?iul 8.1. Suprascrie?i operatorii new ?i delete ?n clasele rational_fara_semn ?i rational. ?n versiunile suprascrise ale acestor operatori modifica?i numai con?inutul mesajelor despre func?ia respectiv?. Rezultatul afi?at de fragmentul precedent dup? exerci?iul dat trebuie s? fie urm?torul:

Sunt in new pentru fr!
Sunt in new pentru rfs!
Sunt in new pentru r!
Sunt in new pentru rfs!
Sunt in new pentru fr!
Sunt in new pentru r!

Vectorul vrp initial:
2/3
1,3/5
-2,1/3
1,1/4
4/5
-1,2/3

Vectorul vrp dupa sortarea:
-2,1/3
-1,2/3
2/3
4/5
1,1/4
1,3/5

Sunt in delete pentru r!
Sunt in delete pentru r!
Sunt in delete pentru fr!
Sunt in delete pentru fr!
Sunt in delete pentru rfs!
Sunt in delete pentru rfs!

Analiza?i acest rezultat ?i face?i concluzii.

S? vedem cum lucreaz? func?iile virtuale cu referin?e. ?n primul r?nd, verific?m c? operatorul de inser?ie << este supra?nc?rcat pentru clasa fractie_rationala ?n fi?ierul ”fr.cpp” astfel:

ostream &operator << (ostream& stream, const fractie_rationala &fr)
{
return fr.print(stream);
}

Dac? este a?a, putem elimina din fi?ierul ”rfs.cpp” supra?nc?rc?rile acestui operator pentru clasele rational_fara_semn ?i rational. Dup? ce func?ia print() a devenit virtual?, nu mai avem nevoie de supra?nc?rcarea operatorului de inser?ie pentru clasele derivate (cu condi?ia c? func?ia virtual? print() va fi suprascris? ?n fiecare clas? derivat?).

Acum verifica?i c? operatorul de inser?ie <<, supra?nc?rcat pentru clasa fractie_rationala de pe atunci c?nd clasele derivate nici nu existau, ?n cazul:

cout << fractie_rationala(3, 5) << endl;
va afi?a:
3/5

?n cazul:
cout << rational_fara_semn(2, 1, 7) << endl;
va afi?a:
2,1/7

iar ?n cazul:
cout << rational(-1, 4, 5) << endl;
va afi?a:
-1,4/5

Este ceva fantastic, nu-i a?a?

Exemplele cercetate de utilizare a func?iilor virtuale au demonstrat esen?a polimorfismului ?n timpul rul?rii.

Descriem propriet??ile func?iilor virtuale:

      • O func?ie membr? poate s? devin? virtual? ?ncep?nd de la orice nivel de ierarhie, ?ns? dac? o func?ie membr? a devenit virtual? la un nivel de ierarhie ?ntr-o clas?, ea va r?m?ne virtual? ?n toate genera?iile viitoare ale acestei clase. Adic?, atributul de virtualitate se mo?tene?te ?mpreun? cu ?ns??i func?ia. De exemplu, ?n clasa rational_fara_semn este func?ia membr? nevirtual? la_fr(). Putem s-o suprascriem ?n clasa derivat? rational ca virtual? (prefix?nd antetul ei cu cuv?ntul-cheie virtual). Atunci, ?n clasa rational_fara_semn func?ia la_fr() r?m?ne nevirtual?, a?a cum a fost; ?n clasa rational func?ia suprascris? la_fr() va fi virtual?. Virtual? ea va fi ?i ?n orice alt? clas? derivat? de la rational, nu conteaz? va fi ea suprascris? sau nu ?n clasa derivat?.
      • Suprascriind o func?ie virtual? ?ntr-o clas? derivat?, se poate omite cuv?ntul-cheie virtual ?n antetul de suprascriere, dar poate fi ?i l?sat, pentru ca lucrurile s? fie mai clare.
      • Dac? vre?i s? suprascrie?i o func?ie virtual? ?ntr-o clas? derivat?, trebuie s? respecta?i antetul func?iei virtuale corespunz?toare din clasa de baz?. Antetul la suprascriere trebuie s? fie exact acela?i, cu excep?ia c? cuv?ntul virtual poate fi omis. De exemplu, func?ia de afi?are ?n clasele fractie_rationala, rational_fara_semn, rational are unul ?i acela?i antet virtual ostream &print(ostream &stream=cout).
      • Orice func?ie virtual? (?n afar? de destructor) a oric?rei clase poate fi supra?nc?rcat?, ?ns? atributul de virtualitate nu se transmite la supra?nc?rcare. Func?ia care supra?ncarc? o func?ie virtual? poate fi ?i ea virtual? (dac? cuv?ntul virtual va fi indicat explicit la supra?nc?rcare). ?ns?, independent de faptul va fi ea virtual? sau nu, func?ia care supra?nc?rc? o func?ie virtual? nu va avea nici o leg?tur? cu ultima. De exemplu, putem s? mai introducem ?n  clasa  rational_fara_semn  o  func?ie  cu  antetul virtual ostream &print(ostream &stream, char *mesaj), care afi?eaz? un mesaj ?nainte de a afi?a valoarea frac?iei. Aceasta func?ie nou? nu va avea nimic comun cu func?ia ini?ial? virtual ostream &print(ostream &stream=cout) ?i nici nu va influen?a asupra exemplelor precedente cu apelurile func?iilor virtuale prin pointeri sau referin?e.
      • Cuv?ntul-cheie virtual se scrie numai ?n antetul func?iei membre ?n declararea clasei. Dac? descrierea func?iei se face aparte, dup? declararea clasei, cuv?ntul virtual a doua oar? nu se mai scrie, exact a?a cum ?i atributul friend la func?ii prietene.
      • Constructorul nu poate fi virtual, pe c?nd destructorul poate s? fie virtual ?i, ?n unele cazuri, chiar trebuie s? fie virtual. G?ndi?i-v? la cazul obiectelor pointate de c?tre vectorul de pointeri vectorp ?i la apelurile destructorilor acestor obiecte ?n destructorul clasei vectorp.
Clasa care con?ine doar o func?ie virtual? se nume?te clas? polimorfic?

Cu toate c? utilizarea func?iilor virtuale aduce un aport esen?ial ?n realizarea principiilor de polimorfism, folosind func?iile virtuale trebuie s? con?tientiz?m faptul c? leg?turile ulterioare reduc viteza de executare a programului. Aceast? reducere poate fi neesen?ial?, dac? ?n program nu sunt multe apeluri ale func?iilor virtuale prin intermediul pointerilor sau referin?elor. Din contra, executarea programului poate fi cu mult mai lent?, dac? apelurile de acest tip se ?nt?lnesc ?n program destul de des, mai ales dac? ele se afl? ?n cicluri mari ?i ad?nci.
_________________________
Autorul: dr.conf. S. Pereteatcu

 

8.4. Func?ii virtuale pure ?i clase abstracte

8.4. Func?ii virtuale pure ?i clase abstracte

?n limbajul C++ a fost introdus? suplimentar o form? special? de func?ii membre virtuale, pentru care nu se indic? corpul func?iei. A?a func?ii virtuale au c?p?tat denumirea de func?ii virtuale pure.

Func?ia virtual? pur? este o func?ie virtual? special? de formavirtual [tip] [nume_clas?::]nume_func?ie ([list?_parametri]) = 0;

sau

tip virtual [nume_clas?::]nume_func?ie ([list?_parametri]) = 0;

Ea nu are corp ?i nu poate fi executat?. Func?ii virtuale pure se declar? ?n clase cu scopul de a fi suprascrise ?n clase derivate respective.

Sunt cel pu?in dou? motive de utilizare a func?iilor virtuale pure:

  • Clasa de baz? nu are suficiente informa?ii ?i nu este ?n stare s? descrie complet func?ionalitatea unei func?ii membre virtuale.
  • Pentru a impune suprascrierea for?at? de c?tre programatori a func?iilor virtuale pure ?n propriile sale clase.

Clasele ce con?in func?ii virtuale pure au c?p?tat denumirea de clase abstracte. De ce abstracte? Pentru c? compilatorul nu permite crearea exemplarelor de obiecte de clase care con?in func?ii virtuale pure. A?a clase sunt folosite numai ca baz? pentru crearea claselor derivate noi.

Clas? care con?ine sau mo?tene?te f?r? suprascriere m?car o func?ie virtual? pur? se nume?te clas? abstract?. Nu se admite crearea exemplarelor de obiecte ale clasei abstracte. Clasa abstract? serve?te numai ca baz? pentru declararea altor clase (derivate).

?ntr-o clas? abstract? putem formula principiile de func?ionare a unei ierarhii de clase, dup? care ?n clasele derivate vom concretiza func?ionalitatea prin suprascrierea func?iilor virtuale pure.

De?i exemplarele de obiecte ale claselor abstracte nu pot fi create, declar?rile pointerilor cu tipul de baz? a unei clase abstracte ?i declar?rile parametrilor-referin?elor la o clas? abstract? sunt perfect valide ?i utilizate la descrierea principiilor de func?ionare a unei ierarhii de clase.

Clasa derivat? nu este obligatorie s? suprascrie toate func?iile virtuale pure mo?tenite de la o clas? abstract?, ?i dac? ea nu face acest lucru complet, ea continu? s? r?m?n? abstract?. Mai mult ca at?t, o clas? derivat? abstract? poate s? defineasc? suplimentar func?ii virtuale pure proprii. ?i numai ?n cazul c?nd ?ntr-o clas? derivat? sunt suprascrise toate func?iile virtuale pure, ?i nu s-au mai ad?ugat func?ii virtuale pure noi, clasa derivat? devine o clas? concret?, nu abstract?, ?i apare posibilitatea de creare a instan?elor acestei clase.

O clas? derivat? care mo?tene?te de la o clas? non-abstract? va fi declarat? ca fiind o clas? abstract?, dac? ?n ea va fi introdus? m?car o func?ie virtual? pur? proprie, sau/?i suprascris? ca pur? m?car o func?ie virtual? mo?tenit?. Aceste reguli r?m?n ?n vigoare ?i ?n cazul mo?tenirii multiple.

Revenim la exemplul nostru. ?n calitate de clas? abstract? putem utiliza no?iunea de un num?r abstract care poate fi convertit la un zecimal ?n virgul? mobil? ?i poate fi afi?at. Inser?m ?n fi?ierul ”fr.cpp”, ?nainte de declararea clasei fractie_rationala, urm?torul fragment care declar? clasa abstract? numar:

class numar
{
   public:
      virtual ostream  &print(ostream &stream) = 0;
      virtual operator double() = 0;
};

Totodat?, modific?m antetul de declarare a clasei fractie_rationala ca el s? aib? urm?toarea redactare:

class fractie_rationala: public numar

Acum, ?n exemplul nostru ierarhia claselor va fi compus? din patru clase. La baza acestei ierarhii st? clasa abstract? numar cu dou? func?ii virtuale pure. Clasa fractie_rationala va fi acum o clas? derivat? de la numar. Clasa fractie_rationala nu va fi abstract?, fiindc? ?n ea sunt suprascrise toate func?iile virtuale pure mo?tenite de la clasa de baz? numar.

Mai departe, f?r? modific?ri, clasa rational_fara_semn mo?tene?te de la fractie_rationala ?i clasa rational se bazeaz? pe clasa rational_fara_semn. Dac? acum lans?m programul cu func?ia main() din fi?ierul ”rfs.cpp”, toate rezultatele vor fi exact acelea?i care au fost ?i ?nainte de ultima modificare.

?n caz de necesitate, func?ia virtual? pur? poate fi definit? de c?tre utilizatorul clasei abstracte ?n orice loc al programului unde poate fi definit? orice func?ie. Forma de definire este urm?toarea:

tip nume_clas? :: nume_func?ie (list?_parametri) {}

Definirea func?iilor virtuale pure nu modific? clasa din abstract? ?n non-abstract?, adic? nu exclude necesitatea de suprascriere a func?iilor virtuale pure ?n clasele derivate pe care dorim s? le prefacem ?n neabstracte.

Pentru exemplu, inser?m ?n fi?ierul ”rfs.cpp” (pentru claritate – ?nainte de declararea clasei rational) urm?toarea defini?ie a func?iei virtuale pure print membr? a clasei numar:

ostream &numar::print(ostream &stream)
{
   return stream << "Q:";
}

Dup? cum observ?m, func?ia transmite ?n streamul de ie?ire stream textul "Q:" ?i ?ntoarce ca rezultat referin?a la acest stream. Modific?m suprascrierea func?iei print() ?n clasa rational, inser?nd apelul func?iei print() a clasei numar:

ostream &print(ostream &stream=cout)
      {
         numar::print(stream); // acest apel a fost inserat
         if(semn=='-')
            stream << '-';
         if (part_int)
            stream << part_int << ",";
         return fractie_rationala::print(stream);
      }

Lans?m programul ?i observ?m efectul. Afi?area oric?rui obiect al clasei rational va fi prefixat? de textul "Q:"(de exemplu, Q:-1,4/5).

Ca ?i orice alt? func?ie virtual?, destructorul clasei abstracte poate fi pur. ?n acest caz el neap?rat trebuie s? fie definit undeva ?n program, fiindc?, dup? cum ?ti?i, destructorul clasei derivate (fie el implicit, fie el definit explicit ?n clasa derivat?) apeleaz? automat destructorul clasei de baz?, ceea ce cere ca destructorul clasei de baz? s? fie definit. Dac? nu defini?i destructorul virtual pur al clasei de baz? abstracte, editorul de leg?turi va da un mesaj de eroare c? nu poate g?si func?ia-destructor a clasei de baz?.

Cu toate c? programul va fi compilat cu succes, editorul de leg?turi nu va crea modulul executabil ?i nu ve?i putea lansa programul ?n execu?ie.

Exerci?iul 8.2. G?ndi?i-v? la cazul c?nd avem un lan? de clase abstracte derivate una de la alta, fiecare cu propriul destructor virtual pur, ?i ultima clas? derivat? nu este abstract?.

?n continuarea exemplului nostru inser?m ?n sec?iunea public a clasei numar (fi?ierul ”fr.cpp”) urm?torul cod:

virtual ~numar() = 0; // destructorul virtual ?i pur

Compilarea programului din fi?ierul ”rfs.cpp” va trece cu succes. ?ns?, dac? vom ?ncerca s? construim modulul executabil, vom primi de la editorul de leg?turi un mesaj de tipul ”Error: Unresolved external 'numar::~numar()'”. Este suficient de a insera defini?ia destructorului:

numar::~numar()
{
 
}

?n orice loc ?n fi?ierul ”fr.cpp” sau ”rfs.cpp”, unde, ?n principiu, poate fi scris? o func?ie (pentru exemplu, dup? func?ia main(), cu toate c? solu?ia aceasta nu este reu?it?), ?i func?ionalitatea programului va fi restabilit?, iar rezultatele vor fi acelea?i ca ?i data trecut?.

Nu v? descuraja?i de faptul c? acest destructor este vid, la necesitate pute?i insera ?n el orice instruc?iuni.
_________________________
Autorul: dr.conf. S. Pereteatcu

Translate Переводчик

Подписка на новости

SmartResponder.ru
Ваш e-mail: *
Ваше имя: *

Хостинг для Wordpress сайтов