#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:
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
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!