Table of Contents

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++:

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:

2. Legarea Statica

Legarea statica (Early binding):

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:

In cazul apelului prin intermediul pointerilor.

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

Procedeul este similar:

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):

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:

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.