Exemplu: colectie de instrumente muzicale

#include <iostream>
using namespace std;
enum note {re, mi, fa, sol, la, si, do};
class Instrument{
	public:
		void play(note) const{
			cout << "Instrument::play" << endl;
		};
};
class Vioara: public Instrument{
	public:
		void play(note) const{
			cout << "Vioara::play" << endl;
		};
};
void tune(Instrument& i){
	i.play(sol);
};
int main(){
	Vioara v;
	tune(v);             //upcast 
	Instrument* ip = &v; //upcast
	ip->play(re);
	Instrument& ir = v;  //upcast
	ir.play(mi);
}
Trecerea de la un tip specific (vioara) la un tip mai general (instrument), este o operatie sigura, numita upcast. Se pierde insa informatia despre tipul obiectului, iar compilatorul poate gestiona obiectul DOAR prin intermediul pointerului/referintei la clasa generala; astfel, se va apela metoda play() din clasa Instrument! Motivul acestui comportament este rezolvarea apelului de functie la compilare si link-editare, tehnica numita early binding.

Mecanismul functiilor virtuale implementeaza tehnica late binding: rezolvarea apelului de functie in timpul executiei programului. In acest scop, compilatorul insereaza in program cod prin care se determina, la executie, care este definitia corecta a functiei.

O functie membru este virtuala daca declaratia sa este precedata de cuvintul cheie virtual. O functie, declarata virtuala intr-o clasa de baza, este virtuala in toate clasele derivate.


virtual void play(note)const {/**/};
Pentru fiecare clasa care contine macar o functie virtuala se creeaza o tabela a functiilor virtuale, numita VTABLE. Tabela contine adresele definitiilor functiilor membru virtuale. Toate instantele (obiectele) unei astfel de clase contin o data membru suplimentara, numita VPTR, care nu este altceva decat un pointer catre tabela VTABLE. La apelul unei functii virtuale prin intermediul unui pointer/referinte la clasa de baza compilatorul insereaza cod prin care, la executie, dereferentiind VPTR, se acceseaza VTABLE si se gaseste adresa functiei.

Exemplu: dimensiunea claselor cu si fara functii virtuale


#include <iostream>
class NoVirtual{
	int a;
	public:
	void x() const{};
	int i() const{
		return 1;
	};
};
class OneVirtual{
	int a;
	public:
	virtual void x() const{};
	int i() const{
		return 1
	};
};
class TwoVirtuals{
	int a;
	public:
	virtual void x() const{};
	virtual int i(){
		return 1;
	};
};
int main(){
	cout << "int:" << sizeof(int) << endl;
	cout << "NoVirtual:" << sizeof(NoVirtual) << endl;
	cout << "void*:" << sizeof(void*) << endl;
	cout << "OneVirtual:" << sizeof(OneVirtual) << endl;
	cout << "TwoVirtuals:" << sizeof(TwoVirtuals) << end;
}
Se observa imediat ca: Pentru fiecare instanta (obiect) a unei clase, VPTR trebuie initializat inainte de a se apela vre-o functie virtuala; din acest motiv constructorii NU pot fi functii virtuale. Deoarece, intr-o ierarhie de clase, destructorii se apeleaza in ordine inversa ordinii de apel a constructorilor (iar VPTR este deja initializat), este recomandat ca destructorii sa fie functii virtuale! (un destructor ne-virtual semnalizeaza intentia de a nu folosi acesta clasa drept clasa de baza)

Exemplu:


#include <iostream>
class Base1{
	public:
	~Base1(){
		cout << "~Base1()" << endl;
	};
};
class Base2{
	public:
	virtual ~Base2(){
		cout << "~Base2()" << endl;
	};
};
class Derived1: public Base1{
	public:
	~Derived1(){
		cout << "~Derived1()" << endl;
	};
};
class Derived2: public Base2{
	public:
	~Derived2(){
		cout << "~Derived2()" << endl;
	};
};
int main(){
	Base1* pb1 = new Derived1;
	delete pb1;
	Base2* pb2 = new Derived2;
	delete pb2;
}
La executia programului se obseva ca delete bp1 apeleaza doar destructorul clasei de baza, in timp ce delete bp2 apeleaza, corect, destructorul clasei derivate, urmat de destructorul clasei de baza. Practic, desi destructorul nu se mosteneste, destructorul virtual este supraincarcat in clasa derivata!

O functie virtuala pura este o functie virtuala pentru care nu se declara o implementare (se mosteneste doar interfata). Sintactic, lucrurile arata astfel:


virtual tip_returnat nume(...)=0;
Din punct de vedere practic, compilatorul rezerva in VTABLE cate o locatie "goala" pentru fiecare functie virtuala pura. O clasa care contine cel putin o functie virtuala pura este o clasa abstracta si nu poate fi instantiata!

Clasele care mostenesc clase abstracte trebuie sa implementeze toate functiile virtuale pure; in caz contrar, sunt si ele clase abstracte. Aceasta deoarece se copie VTABLE din clasa de baza si se modifica doar adresele functiilor virtuale supraincarcate! De aici rezulta si faptul ca, daca o functie virtuala nu este supraincarcata, clasa derivata are acces la definitia functiei din clasa imediat superioara in ierarhie! Cu alte cuvinte, se mosteneste intotdeauna interfata (prototipul) si, optional, implementarea. Ultima supraincarcare a unei functii virtuale se numeste last overrriden

idee.jpg - 2332 Bytes Un destructor poate fi virtual pur! Insa destructorul unei clase derivate trebuie sa poata apela destructorul clasei de baza! Din acest motiv, un destructor virtual pur trebuie sa aiba o implementare (vida).


class Interface{
	public:
	virtual void Open() = 0;
	virtual ~Interface() = 0;
};
Interface::~Interface(){} //implementare vida
Daca supraincarcarea unei functii virtuale necesita extinderea versiunii din clasa de baza (adica adaugarea de functionalitate definitiei deja existente), se poate proceda astfel:

class shape{
        public:
        virtual void resize(int x, int y){
                clearscr();
        };
};
class rectangle: public shape{
        public:
        virtual void resize (int x, int y){
                shape::resize();                //legare statica!
                //adauga functionalitate...
        };
};
Schimbarea specificatiilor de acces la o functie virtuala nu este o idee prea buna; se considera urmatorul exemplu:

class base{
        public:
        virtual void say(){
                cout << "base" << endl;
        };
};
class derived: public base{
        void say(){ //say() este acum private
                cout << "derived" << endl;
        };
};
...
derived d;
base* pb = &d;
pb->say(); //OK, se apeleaza derived::say()
Deoarece say() este functie virtuala, legarea tarzie impiedica compilatorul sa detecteze apelul unei functii ne-publice!

  1. Pot exista functii virtuale ne-membru?
  2. Pot exista functii virtuale statice?
  3. Pot exista functii virtuale inline?
  4. Ce semnifica o functie virtuala pura careia i se precizeaza o implementare?