Laboratorul 07: Functii Virtuale

In cadrul acestui laborator, vom aprofunda inca un concept important al Programarii Orientate pe Obiecte, functii virtuale.

Ca referinte externe, recomandam urmatorul capitol din Absolute C++:

  • Capitolul 15 (Chapter 15: Polymorphism and Virtual Functions, pag. 661 - 693)

1. Introducere

Functiile virtuale permit claselor derivate sa inlocuiasca implementarea metodelor din clasa de baza - suprascriere/supraincarcare/override- si pun la dispozitie mecanismul de legare dinamica.

O functie virtuala este membra a clasei de baza si este redefinita(overriden) de o clasa derivata.

Pentru a intelege mai bine importanta folosirii functiilor virtuale, introducem conceptul de binding(legare).

Legarea (Binding) reprezinta conectarea unui apel de functie cu functia in sine (adresa functiei).

Legarea poate fi:

  • Statica/timpurie (la compilare)
  • Dinamica/tarzie (in momentul executie)

2. Legarea Statica

Legarea statica (Early binding):

  • se realizeaza la compilare (compile time, inainte de rularea aplicatiei)
  • este realizata de catre compilator si editorul de legaturi (linker)

Asa cum sugereaza si numele, compilatorul(sau linker-ul) asociaza in mod direct apelului de functie o adresa.

Orice apel normal de functie(fara virtual)este legat static.

In C toate apelurile de functii presupun realizarea unei legaturi statice.

In C++

Functiile membre ale unei clase primesc adresa obiectului care face apelul.

tip_date obiect; 
obiect.functie();

In functie de tipul obiectului de la acea adresa - compilatorul si editorul de legaturi stabilesc daca:

  • nume_functie() e membra a clasei tip_date (e declarata)
  • este implementata
  • daca este implementata, se face legarea statica si se poate apela acea functie
  • daca functia era declarata ca membra a clasei, dar nu e implementata arunca eroarea ‎‎‏‏‎ ‎‏‏‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏ ‎‏‏‎ ‎‏‎‏‏‎ ‎‏‏‎‏‏‎ ‎‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎[Linker error] undefined reference to 'tip_date::nume_functie()'

In cazul apelului prin intermediul pointerilor.

tip_date_baza *obiect = new tip_date_baza;
obiect->functie();

Procedeul este similar:

  • compilatorul si editorul de legaturi stabilesc daca functia invocata de un pointer este de tipul acelui pointer (daca poate fi apelata)
  • compilatorul foloseste tipul static al pointerului pentru a determina daca invocarea functiei membre este legala.
Exemplu
Early_Binding.cpp
#include<iostream> 
using namespace std; 
 
class Baza
{ 
public: 
    void afisare() { cout<<" In Baza \n"; } 
}; 
 
class Derivata: public Baza
{ 
public: 
    void afisare() { cout<<"In Derivata \n"; } 
}; 
 
int main(void) 
{ 
    Baza *bp = new Derivata; 
 
    // Compilatorul vede tipul pointerului si
    // apeleaza functia din clasa Baza
    bp->afisare();   
 
    return 0; 
} 

In urma rularii programului, se va afisa:

 In Baza 

Daca vrem sa apelam functia de afisare pentru obiectul catre care pointeaza bp, este necesara o conversie explicita a pointerului bp din (Baza*) in (Derivata*), astfel incat legatura sa se faca pentru tipul de date al obiectului catre care pointeaza bp.

 ((Derivata*)bp)->afisare(); 

Dar

  • aceasta solutie nu e robusta
  • trebuie mereu sa ne punem problema catre ce tip de obiect pointeaza pointerul de tip clasa de baza si sa facem conversii explicite pentru a apela functia dorita
  • e predispusa erorilor logice

Alternativa pusa la dispozitie in C++ este mecanismul de legare dinamica/tarzie (dynamic/late binding) - nu trebuie sa memorez catre ce tip de obiect se pointeaza in timpul executiei

3. Legarea Dinamica

Legare dinamica/tarzie(Late binding):

  • legarea se face dupa compilare, la rulare
  • in functie de tipul dinamic al pointerului (tipul obiectului catre care se pointeaza)
  • se poate realiza doar in contextul apelului de functii prin intermediul pointerilor

In acest caz, compilatorul identifica tipul obiectului la momentul rularii si apoi apeleaza functia potrivita.

Pentru a folosi acest tip de legare, compilatorul trebuie informat ca exista posibilitate ca, la rulare, sa se doreasca apelarea unei functii de tipul dinamic al obiectului.

Pentru acest lucru vom introduce un semnal in cod → functii virtuale

Legarea dinamica poate fi implementata doar in cazul limbajelor compilate!

Exemplu
Late_Binding.cpp
#include<iostream> 
using namespace std; 
 
class Baza 
{ 
public: 
    virtual void afisare() { cout<<" In Baza \n"; } 
}; //legarea pentru functia afisare se face la rulare
 
class Derivata: public Baza
{ 
public: 
    void afisare() { cout<<"In Derivata \n"; }  //supraincarcare
};  //legarea pentru functia afisare se face la rulare; e virtuala
 
 
int main(void) 
{ 
    Baza *bp = new Derivata; //legare dinamica (in functie de tipul dinamic), apel Derivata::afisare() 
    bp->afisare(); 
    return 0; 
} 

Observam asadar ca de data aceasta, se va afisa

 In Derivata 

Legarea dinamica se poate face doar folosind functii virtuale si pointeri.

Dupa cum am afirmat deja, cuvantul cheie virtual informeaza compilatorul sa nu realizeze o legatura statica. Dar cum reuseste acesta sa puna in actiune toate mecanismele necesare realizarii de legaturi dinamice?

Pentru aceasta, compilatorul creeaza un tabel numit VTABLE pentru fiecare clasa care contine macar o functie virtuala.

In VTABLE sunt puse adresele functiilor virtuale ale acelei clase. Fiecare obiect de tipul clasei cu functii virtuale va avea in posesie un pointer numit vpointer (sau VPTR) catre adresa lui VTABLE.

Crearea VTABLE –ului pentru clase si initializarea VPTR –ului este automata (facuta de compilator)

Cand se face initializarea VPTR-ului?

In momentul crearii unui obiect. Mai exact la apelul constructorului. In cazul in care nu se implementeaza niciun constructor, cel generat automat face si aceasta initializare.

Functiile statice NU pot sa fie virtuale pentru ca functiile statice nu sunt apelate prin intermediul unui obiect si nu primesc adresa unui obiect si astfel nu au acces la VPTR.

4. Polimorfism si functii virtuale

“poli” – mai multe; “morf” – forma

Polimorfismul se poate realiza prin:

  • supradefinirea functiilor - functii cu acelasi nume, dar semnaturi diferite - care se comporta diferit in functie de context – in functie de modul in care sunt apelate – chiar daca au acelasi nume; polimorfism ad hoc
  • suprascrierea functiilor – functii virtuale: acelasi pointer poate sa aiba comportamente diferite la apelul unei metode, in functie de tipul lui dinamic

Avand in vedere importanta lor, de ce nu folosim exclusiv functii virtuale? De ce nu realizeaza C++ decat legaturi dinamice?

★Pentru ca legarea dinamica nu este la fel de eficienta ca legarea statica (asa ca o folosim doar cand e nevoie).

Cand folosim functii virtuale atunci?

★De fiecare data cand vrem ca in clasa derivata sa modificam/adaptam/suprascriem comportamentul unei functii deja implementate in clasa de baza.

Cand este important acest mecanism?

  1. Functiile virtuale sunt folosite pentru a implementa polimorfismul in momentul rularii (” Run time Polymorphism“)
  2. Un alt avantaj al functiilor virtuale este ca permit realizarea de liste neomogene de obiecte (exemplul de la finalul cursului C6)
  3. Dezvoltarea de biblioteci in spiritul POO.

Exemplu
Baza.h
#pragma once
#include <iostream>
using namespace std;
class Baza{
    protected:
        int atr1;
    public:
        Baza();
        Baza(int );
        void set_atr1(int );
        virtual void afisare();
        virtual ~Baza(){};
};
Baza.cpp
#include "Baza.h"
    Baza::Baza() { }
    Baza::Baza(int i):atr1(i) { }
    void Baza::set_atr1(int i) { 
        atr1 = i; 
    }
    void Baza::afisare() { 
        cout << "atr1 = " << atr1 << endl; 
    }
Derivata.h
#pragma once
#include "Baza.h"
class Derivata : public Baza {
    protected: 
        int atr2;
    public:
        Derivata();
        Derivata(int , int );
        void set_atr2(int );
        void afisare(); //afisare din Derivata e virtuala
};//destructorul generat automat e virtual
Derivata.cpp
#include "Derivata.h"
    Derivata::Derivata() { }
    Derivata::Derivata(int a1, int a2):Baza(a1),atr2(a2) { }
    void Derivata::set_atr2(int n) { 
        atr2 = n; 
    }
    void Derivata::afisare() { 
        Baza::afisare();
        cout << "atr2 = " << atr2 << endl;
    }
main.cpp
#include "Derivata.h"
int main(int argc, char *argv[]) {
    int n;
    cout << "Dati dimensiunea";
    cin >> n;
    Baza **vec = new Baza*[n];
    for (int i = 0; i < n; i++){
        cout << "Introduceti obiect de tip Baza(0) sau Derivata(1)?";
        int tip;
        cin >> tip;
        if (tip == 0){
            cout << "Dati atr1:";
            int a1;
            cin >> a1;
            vec[i] = new Baza(a1);
        }
        else if (tip == 1){
            cout << "Dati atr1 si atr2:";
            int a1, a2;
            cin >> a1; cin >> a2;
            vec[i] = new Derivata(a1,a2);
        } else i--;
    } 
    for (int i = 0; i < n; i++)
         vec[i]->afisare();
    //comportament polimorf; nu mai testez eu care e tipul si nu fac conversii explicite
    //Baza *b = new Derivata(1,1);
    // b->set_atr2(2);
    //atentie, set_atr2 nu e o functie din Baza
    //ERROR:'class Baza' has no member named 'set_atr2'
    return 0;
}

Recomandari

  1. Se recomanda sa se declare ca virtuale, functiile care, in derivari ulterioare NU isi schimba semnificatia (acelasi nume, semnatura, tip returnat), ci doar li se modifica / adapteaza implementarea /comportamentul (in functie de noul tip de date).
  2. Daca o clasa are macar o functie virtuala, destructorul trebuie declarat virtual.

poo-is-aa/laboratoare/07.txt · Last modified: 2024/08/14 20:10 (external edit)
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